@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,728 @@
1
+ <script lang="ts">
2
+ import type { FileNode } from '$shared/types/filesystem';
3
+ import LoadingSpinner from '../common/LoadingSpinner.svelte';
4
+ import MonacoEditor from '../common/MonacoEditor.svelte';
5
+ import { themeStore } from '$frontend/lib/stores/ui/theme.svelte';
6
+ import Icon from '$frontend/lib/components/common/Icon.svelte';
7
+ import { getFileIcon } from '$frontend/lib/utils/file-icon-mappings';
8
+ import { getFolderIcon } from '$frontend/lib/utils/folder-icon-mappings';
9
+ import { onMount, onDestroy } from 'svelte';
10
+ import type { IconName } from '$shared/types/ui/icons';
11
+ import type { editor } from 'monaco-editor';
12
+ import { debug } from '$shared/utils/logger';
13
+ import ws from '$frontend/lib/utils/ws';
14
+
15
+ // Interface untuk MonacoEditor component
16
+ interface MonacoEditorComponent {
17
+ getEditor: () => editor.IStandaloneCodeEditor | null;
18
+ getValue: () => string;
19
+ setValue: (newValue: string) => void;
20
+ getLanguage: () => string;
21
+ setLanguage: (newLanguage: string) => void;
22
+ detectLanguageFromFilename: (filename: string) => string;
23
+ focus: () => void;
24
+ layout: () => void;
25
+ }
26
+
27
+ interface Props {
28
+ file: FileNode | null;
29
+ content?: string;
30
+ savedContent?: string;
31
+ isLoading?: boolean;
32
+ error?: string;
33
+ onSave?: (filePath: string, content: string) => Promise<void>;
34
+ hideHeader?: boolean;
35
+ targetLine?: number;
36
+ onContentChange?: (content: string) => void;
37
+ wordWrap?: boolean;
38
+ onToggleWordWrap?: () => void;
39
+ externallyChanged?: boolean;
40
+ onForceReload?: () => void;
41
+ }
42
+
43
+ const {
44
+ file = null,
45
+ content = '',
46
+ savedContent: savedContentProp,
47
+ isLoading = false,
48
+ error = '',
49
+ onSave,
50
+ hideHeader = false,
51
+ targetLine = undefined,
52
+ onContentChange,
53
+ wordWrap = false,
54
+ onToggleWordWrap,
55
+ externallyChanged = false,
56
+ onForceReload
57
+ }: Props = $props();
58
+
59
+ // Theme state
60
+ const isDark = $derived(themeStore.isDark);
61
+ const monacoTheme = $derived(isDark ? 'vs-dark' : 'vs-light');
62
+ // Force remount Monaco Editor when theme or file changes
63
+ const themeKey = $derived(`monaco-${monacoTheme}-${file?.path || ''}`);
64
+
65
+ // Edit state - always in edit mode (no toggle)
66
+ let editableContent = $state('');
67
+ let isSaving = $state(false);
68
+ let hasChanges = $state(false);
69
+
70
+ // Derived state for save button
71
+ const canSave = $derived(hasChanges && !isSaving && !!file && !!onSave);
72
+ const saveButtonDisabled = $derived(!canSave);
73
+
74
+ let monacoEditorRef: MonacoEditorComponent | null = $state(null);
75
+
76
+ // Line highlighting state
77
+ let currentDecorations: string[] = $state([]);
78
+
79
+ // Image/binary blob URL state
80
+ let blobUrl = $state<string | null>(null);
81
+
82
+ // SVG view mode
83
+ let svgViewMode = $state<'visual' | 'code'>('visual');
84
+
85
+ // PDF blob URL
86
+ let pdfBlobUrl = $state<string | null>(null);
87
+
88
+ // Container ref for measuring height
89
+ let containerRef = $state<HTMLDivElement | null>(null);
90
+ let editorHeight = $state('0px'); // Default height
91
+
92
+ // Update editor height based on container
93
+ function updateEditorHeight() {
94
+ if (containerRef) {
95
+ const rect = containerRef.getBoundingClientRect();
96
+ // Get actual available height
97
+ const availableHeight = rect.height;
98
+ if (availableHeight > 0) {
99
+ const adjustedHeight = availableHeight;
100
+ editorHeight = `${Math.max(200, adjustedHeight)}px`;
101
+ } else {
102
+ const viewportHeight = window.innerHeight;
103
+ const estimatedHeight = viewportHeight - 200;
104
+ editorHeight = `${Math.max(400, estimatedHeight)}px`;
105
+ }
106
+ }
107
+ }
108
+
109
+ // Update height on mount and resize
110
+ onMount(() => {
111
+ setTimeout(updateEditorHeight, 100);
112
+
113
+ window.addEventListener('resize', updateEditorHeight);
114
+
115
+ function handleKeyDown(e: KeyboardEvent) {
116
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
117
+ e.preventDefault();
118
+ if (canSave) {
119
+ saveChanges();
120
+ }
121
+ }
122
+ }
123
+
124
+ window.addEventListener('keydown', handleKeyDown);
125
+
126
+ if (containerRef && typeof ResizeObserver !== 'undefined') {
127
+ const resizeObserver = new ResizeObserver(() => {
128
+ updateEditorHeight();
129
+ });
130
+ resizeObserver.observe(containerRef);
131
+
132
+ return () => {
133
+ resizeObserver.disconnect();
134
+ window.removeEventListener('resize', updateEditorHeight);
135
+ window.removeEventListener('keydown', handleKeyDown);
136
+ };
137
+ }
138
+
139
+ return () => {
140
+ window.removeEventListener('resize', updateEditorHeight);
141
+ window.removeEventListener('keydown', handleKeyDown);
142
+ };
143
+ });
144
+
145
+ // Cleanup blob URLs on destroy
146
+ onDestroy(() => {
147
+ if (blobUrl) {
148
+ URL.revokeObjectURL(blobUrl);
149
+ }
150
+ if (pdfBlobUrl) {
151
+ URL.revokeObjectURL(pdfBlobUrl);
152
+ }
153
+ });
154
+
155
+ // Update height when container ref changes
156
+ $effect(() => {
157
+ if (containerRef) {
158
+ updateEditorHeight();
159
+ }
160
+ });
161
+
162
+
163
+ // Load binary content (images, PDF) via WebSocket when file changes
164
+ $effect(() => {
165
+ if (file && (isImageFile(file.name) || isPdfFile(file.name))) {
166
+ loadBinaryContent();
167
+ } else {
168
+ // Cleanup if not binary
169
+ if (blobUrl) {
170
+ URL.revokeObjectURL(blobUrl);
171
+ blobUrl = null;
172
+ }
173
+ if (pdfBlobUrl) {
174
+ URL.revokeObjectURL(pdfBlobUrl);
175
+ pdfBlobUrl = null;
176
+ }
177
+ }
178
+ });
179
+
180
+ async function loadBinaryContent() {
181
+ if (!file) return;
182
+
183
+ try {
184
+ const response = await ws.http('files:read-content', { path: file.path });
185
+
186
+ if (response.content) {
187
+ // Decode base64 to binary
188
+ const binaryString = atob(response.content);
189
+ const bytes = new Uint8Array(binaryString.length);
190
+ for (let i = 0; i < binaryString.length; i++) {
191
+ bytes[i] = binaryString.charCodeAt(i);
192
+ }
193
+ const blob = new Blob([bytes], { type: response.contentType || 'application/octet-stream' });
194
+
195
+ if (isPdfFile(file.name)) {
196
+ if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl);
197
+ pdfBlobUrl = URL.createObjectURL(blob);
198
+ } else {
199
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
200
+ blobUrl = URL.createObjectURL(blob);
201
+ }
202
+ }
203
+ } catch (error) {
204
+ debug.error('file', 'Failed to load binary content:', error);
205
+ }
206
+ }
207
+
208
+ // Reference content for change detection (use savedContent if provided)
209
+ const referenceContent = $derived(savedContentProp !== undefined ? savedContentProp : content);
210
+
211
+ // Sync editable content when file or content changes (not when user types)
212
+ let lastSyncedContent = '';
213
+ let lastSyncedFilePath = '';
214
+ $effect(() => {
215
+ const currentFilePath = file?.path || '';
216
+ // Force sync when file changes OR when content changes
217
+ if (content !== lastSyncedContent || currentFilePath !== lastSyncedFilePath) {
218
+ lastSyncedContent = content;
219
+ lastSyncedFilePath = currentFilePath;
220
+ editableContent = content;
221
+ hasChanges = content !== referenceContent;
222
+
223
+ // Directly update Monaco editor to ensure content syncs
224
+ // (bypasses reactive bind:value chain which may not flush in async contexts)
225
+ const editor = monacoEditorRef?.getEditor();
226
+ if (editor && editor.getValue() !== content) {
227
+ editor.setValue(content);
228
+ }
229
+ }
230
+ });
231
+
232
+ // Handle content changes from editor
233
+ function handleContentChange(newContent: string) {
234
+ hasChanges = newContent !== referenceContent;
235
+ onContentChange?.(newContent);
236
+ }
237
+
238
+ // Update Monaco word wrap when prop changes
239
+ // Read wordWrap BEFORE the if-check so it's always tracked by $effect
240
+ $effect(() => {
241
+ const wrapValue: 'on' | 'off' = wordWrap ? 'on' : 'off';
242
+ const editor = monacoEditorRef?.getEditor();
243
+ if (editor) {
244
+ editor.updateOptions({ wordWrap: wrapValue });
245
+ }
246
+ });
247
+
248
+ // Handle line highlighting when targetLine changes
249
+ $effect(() => {
250
+ if (targetLine !== undefined && targetLine > 0) {
251
+ setTimeout(() => {
252
+ const editor = monacoEditorRef?.getEditor();
253
+ if (editor) {
254
+ editor.revealLineInCenter(targetLine);
255
+
256
+ const newDecorations = editor.deltaDecorations(currentDecorations, [
257
+ {
258
+ range: {
259
+ startLineNumber: targetLine,
260
+ startColumn: 1,
261
+ endLineNumber: targetLine,
262
+ endColumn: 1
263
+ },
264
+ options: {
265
+ isWholeLine: true,
266
+ className: 'line-highlight',
267
+ marginClassName: 'line-highlight-margin'
268
+ }
269
+ }
270
+ ]);
271
+
272
+ currentDecorations = newDecorations;
273
+
274
+ setTimeout(() => {
275
+ if (editor) {
276
+ editor.deltaDecorations(currentDecorations, []);
277
+ currentDecorations = [];
278
+ }
279
+ }, 3000);
280
+ }
281
+ }, 100);
282
+ }
283
+ });
284
+
285
+ // Save changes
286
+ async function saveChanges() {
287
+ if (!file || !onSave || !hasChanges) {
288
+ return;
289
+ }
290
+
291
+ isSaving = true;
292
+ try {
293
+ await onSave(file.path, editableContent);
294
+ hasChanges = false;
295
+ } catch (error) {
296
+ debug.error('file', 'Failed to save file:', error);
297
+ } finally {
298
+ isSaving = false;
299
+ }
300
+ }
301
+
302
+ // Get detected language from filename
303
+ function getDetectedLanguage(): string {
304
+ if (!file) return 'plaintext';
305
+
306
+ const ext = file.name.split('.').pop()?.toLowerCase();
307
+ if (!ext) return 'plaintext';
308
+
309
+ const languageMap: Record<string, string> = {
310
+ js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
311
+ mjs: 'javascript', cjs: 'javascript',
312
+ html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'sass', less: 'less',
313
+ py: 'python', pyx: 'python', pyi: 'python',
314
+ java: 'java', c: 'c', cpp: 'cpp', cxx: 'cpp', cc: 'cpp',
315
+ h: 'c', hpp: 'cpp', hxx: 'cpp',
316
+ cs: 'csharp', csx: 'csharp', go: 'go', rs: 'rust',
317
+ php: 'php', phtml: 'php', rb: 'ruby', rbw: 'ruby',
318
+ swift: 'swift', kt: 'kotlin', kts: 'kotlin',
319
+ scala: 'scala', sc: 'scala', r: 'r',
320
+ sh: 'shell', bash: 'shell', zsh: 'shell', fish: 'shell',
321
+ ps1: 'powershell', psm1: 'powershell', bat: 'bat', cmd: 'bat',
322
+ sql: 'sql', xml: 'xml', xsd: 'xml', xsl: 'xml',
323
+ json: 'json', jsonc: 'json', yaml: 'yaml', yml: 'yaml',
324
+ toml: 'toml', ini: 'ini', cfg: 'ini', conf: 'ini',
325
+ md: 'markdown', markdown: 'markdown',
326
+ dockerfile: 'dockerfile', lua: 'lua',
327
+ pl: 'perl', pm: 'perl', hs: 'haskell',
328
+ fs: 'fsharp', fsx: 'fsharp', clj: 'clojure', cljs: 'clojure',
329
+ erl: 'erlang', ex: 'elixir', exs: 'elixir',
330
+ dart: 'dart', sol: 'solidity',
331
+ graphql: 'graphql', gql: 'graphql',
332
+ svelte: 'html', vue: 'html',
333
+ gitignore: 'plaintext', env: 'plaintext', txt: 'plaintext', log: 'plaintext',
334
+ svg: 'xml'
335
+ };
336
+
337
+ return languageMap[ext] || 'plaintext';
338
+ }
339
+
340
+ // Helper functions
341
+ function getDisplayIcon(fileName: string, isDirectory: boolean): IconName {
342
+ if (isDirectory) {
343
+ return getFolderIcon(fileName, false);
344
+ }
345
+ return getFileIcon(fileName);
346
+ }
347
+
348
+ function isImageFile(fileName: string): boolean {
349
+ const extension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
350
+ return ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp'].includes(extension);
351
+ }
352
+
353
+ function isSvgFile(fileName: string): boolean {
354
+ return fileName.toLowerCase().endsWith('.svg');
355
+ }
356
+
357
+ function isPdfFile(fileName: string): boolean {
358
+ return fileName.toLowerCase().endsWith('.pdf');
359
+ }
360
+
361
+ function isBinaryFile(fileName: string): boolean {
362
+ const extension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
363
+ return ['.doc', '.docx', '.xls', '.xlsx', '.zip', '.tar', '.gz', '.exe', '.dll',
364
+ '.ppt', '.pptx', '.7z', '.rar', '.bz2', '.woff', '.woff2', '.ttf', '.eot', '.otf',
365
+ '.mp3', '.mp4', '.wav', '.avi', '.mkv', '.flv', '.sqlite', '.db'].includes(extension);
366
+ }
367
+
368
+ function isVisualFile(fileName: string): boolean {
369
+ return isImageFile(fileName) || isSvgFile(fileName) || isPdfFile(fileName);
370
+ }
371
+
372
+ function formatFileSize(bytes: number): string {
373
+ if (bytes === 0) return '0 B';
374
+ const k = 1024;
375
+ const sizes = ['B', 'KB', 'MB', 'GB'];
376
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
377
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
378
+ }
379
+
380
+ function copyToClipboard() {
381
+ if (editableContent) {
382
+ navigator.clipboard.writeText(editableContent);
383
+ }
384
+ }
385
+
386
+ function downloadFile() {
387
+ if (file) {
388
+ if (editableContent) {
389
+ const blob = new Blob([editableContent], { type: 'text/plain' });
390
+ const url = URL.createObjectURL(blob);
391
+ const a = document.createElement('a');
392
+ a.href = url;
393
+ a.download = file.name;
394
+ a.click();
395
+ URL.revokeObjectURL(url);
396
+ }
397
+ }
398
+ }
399
+ </script>
400
+
401
+ {#if file}
402
+ <div class="w-full h-full flex flex-col">
403
+ <!-- Header -->
404
+ {#if !hideHeader}
405
+ <div class="flex-shrink-0 flex items-center justify-between px-4 py-2.5 border-b border-slate-200 dark:border-slate-700">
406
+ <div class="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
407
+ <Icon name={getDisplayIcon(file.name, file.type === 'directory')} class="w-7 h-7" />
408
+ <div class="min-w-0 flex-1">
409
+ <h3 class="text-xs sm:text-sm font-bold text-slate-900 dark:text-slate-100 truncate">
410
+ {file.name}
411
+ </h3>
412
+ <p class="text-xs text-slate-600 dark:text-slate-400 truncate mt-0.5">
413
+ <span class="hidden sm:inline">{file.path} • </span> {formatFileSize(file.size || 0)}
414
+ </p>
415
+ </div>
416
+ </div>
417
+
418
+ <div class="flex items-center gap-1.5 sm:gap-1 flex-shrink-0">
419
+ <!-- SVG view mode toggle -->
420
+ {#if file && file.type === 'file' && isSvgFile(file.name)}
421
+ <div class="flex bg-slate-100 dark:bg-slate-800 rounded-lg overflow-hidden mr-1">
422
+ <button
423
+ class="flex px-2 py-1.5 text-xs font-medium transition-colors {svgViewMode === 'visual' ? 'bg-violet-600 text-white' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'}"
424
+ onclick={() => { svgViewMode = 'visual'; }}
425
+ title="Visual preview"
426
+ >
427
+ <Icon name="lucide:eye" class="w-3.5 h-3.5" />
428
+ </button>
429
+ <button
430
+ class="flex px-2 py-1.5 text-xs font-medium transition-colors {svgViewMode === 'code' ? 'bg-violet-600 text-white' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'}"
431
+ onclick={() => { svgViewMode = 'code'; }}
432
+ title="Code view"
433
+ >
434
+ <Icon name="lucide:code" class="w-3.5 h-3.5" />
435
+ </button>
436
+ </div>
437
+ {/if}
438
+
439
+ <!-- External change badge + refresh button -->
440
+ {#if externallyChanged && onForceReload}
441
+ <div class="flex items-center gap-1 mr-1">
442
+ <span class="text-[10px] px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-400 font-medium whitespace-nowrap">
443
+ Changed externally
444
+ </span>
445
+ <button
446
+ class="flex p-1.5 text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-900/30 rounded-lg transition-all duration-200"
447
+ onclick={onForceReload}
448
+ title="Reload file from disk (discard local changes)"
449
+ >
450
+ <Icon name="lucide:refresh-cw" class="w-4 h-4" />
451
+ </button>
452
+ </div>
453
+ {/if}
454
+
455
+ <!-- Actions for editable files -->
456
+ {#if file && file.type === 'file' && !isImageFile(file.name) && !isBinaryFile(file.name) && !isPdfFile(file.name) && !(isSvgFile(file.name) && svgViewMode === 'visual')}
457
+ <!-- Word Wrap toggle -->
458
+ {#if onToggleWordWrap}
459
+ <button
460
+ class="flex p-2 rounded-lg transition-all duration-200
461
+ {wordWrap ?
462
+ 'text-violet-600 dark:text-violet-400 bg-violet-100 dark:bg-violet-900/50' :
463
+ 'text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30'
464
+ }"
465
+ onclick={onToggleWordWrap}
466
+ title="Toggle Word Wrap"
467
+ >
468
+ <Icon name="lucide:wrap-text" class="w-4 h-4" />
469
+ </button>
470
+ {/if}
471
+ <!-- Save button -->
472
+ <button
473
+ class="flex p-2 text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:bg-green-50 dark:hover:bg-green-900/30 rounded-lg transition-all duration-200 {saveButtonDisabled ? 'opacity-50 cursor-not-allowed' : ''}"
474
+ onclick={() => {
475
+ if (canSave) {
476
+ saveChanges();
477
+ }
478
+ }}
479
+ disabled={saveButtonDisabled}
480
+ title={saveButtonDisabled ? (hasChanges ? 'Saving...' : 'No changes to save') : 'Save changes (Ctrl+S)'}
481
+ >
482
+ {#if isSaving}
483
+ <div class="w-4 h-4 border-2 border-green-600 border-t-transparent rounded-full animate-spin"></div>
484
+ {:else}
485
+ <Icon name="lucide:save" class="w-4 h-4" />
486
+ {/if}
487
+ </button>
488
+
489
+ {#if editableContent}
490
+ <button
491
+ class="flex p-2 text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30 rounded-lg transition-all duration-200"
492
+ onclick={copyToClipboard}
493
+ title="Copy content"
494
+ >
495
+ <Icon name="lucide:copy" class="w-4 h-4" />
496
+ </button>
497
+ {/if}
498
+
499
+ <button
500
+ class="flex p-2 text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30 rounded-lg transition-all duration-200"
501
+ onclick={downloadFile}
502
+ title="Download file"
503
+ >
504
+ <Icon name="lucide:download" class="w-4 h-4" />
505
+ </button>
506
+ {:else if file && file.type === 'file'}
507
+ <!-- Non-editable file actions -->
508
+ <button
509
+ class="flex p-2 text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30 rounded-lg transition-all duration-200"
510
+ onclick={downloadFile}
511
+ title="Download file"
512
+ >
513
+ <Icon name="lucide:download" class="w-4 h-4" />
514
+ </button>
515
+ {/if}
516
+ </div>
517
+ </div>
518
+ {/if}
519
+
520
+ <!-- Content -->
521
+ <div class="flex-1 overflow-hidden">
522
+ {#if isLoading}
523
+ <div class="flex items-center justify-center h-full">
524
+ <LoadingSpinner size="lg" />
525
+ </div>
526
+ {:else if error}
527
+ <div class="flex flex-col items-center justify-center h-full p-8">
528
+ <Icon name="lucide:triangle-alert" class="w-16 h-16 text-red-400 mb-4" />
529
+ <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
530
+ Unable to load file
531
+ </h3>
532
+ <p class="text-sm text-slate-500 dark:text-slate-400 text-center">
533
+ {error}
534
+ </p>
535
+ </div>
536
+ {:else if file.type === 'directory'}
537
+ <div class="flex flex-col items-center justify-center h-full p-8">
538
+ <Icon name="lucide:folder" class="w-16 h-16 text-slate-400 mb-4" />
539
+ <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
540
+ Directory Selected
541
+ </h3>
542
+ <p class="text-sm text-slate-500 dark:text-slate-400 text-center">
543
+ This is a directory. Select a file to view its content.
544
+ </p>
545
+ </div>
546
+ {:else if isImageFile(file.name)}
547
+ <!-- Image preview -->
548
+ <div class="flex items-center justify-center h-full p-4 overflow-hidden">
549
+ {#if blobUrl}
550
+ <img
551
+ src={blobUrl}
552
+ alt={file.name}
553
+ class="max-w-full max-h-full object-contain"
554
+ />
555
+ {:else}
556
+ <LoadingSpinner size="lg" />
557
+ {/if}
558
+ </div>
559
+ {:else if isSvgFile(file.name)}
560
+ <!-- SVG: visual or code view -->
561
+ {#if svgViewMode === 'visual'}
562
+ <div class="flex items-center justify-center h-full p-4 overflow-auto bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Crect%20width%3D%2220%22%20height%3D%2220%22%20fill%3D%22%23f0f0f0%22%2F%3E%3Crect%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23e0e0e0%22%2F%3E%3C%2Fsvg%3E')]">
563
+ {#if blobUrl}
564
+ <img
565
+ src={blobUrl}
566
+ alt={file.name}
567
+ class="max-w-full max-h-full object-contain"
568
+ />
569
+ {:else if content}
570
+ <!-- Render SVG from content directly -->
571
+ <div class="max-w-full max-h-full flex items-center justify-center">
572
+ {@html content}
573
+ </div>
574
+ {:else}
575
+ <LoadingSpinner size="lg" />
576
+ {/if}
577
+ </div>
578
+ {:else}
579
+ <!-- SVG code view (editable) -->
580
+ <div class="h-full flex flex-col" bind:this={containerRef}>
581
+ <div class="flex-1 bg-slate-50 dark:bg-slate-950 overflow-hidden">
582
+ <div class="h-full flex flex-col">
583
+ <div class="flex-1">
584
+ {#key themeKey}
585
+ <MonacoEditor
586
+ bind:this={monacoEditorRef}
587
+ bind:value={editableContent}
588
+ language="xml"
589
+ readonly={false}
590
+ onChange={handleContentChange}
591
+ height={editorHeight}
592
+ options={{
593
+ minimap: { enabled: false },
594
+ wordWrap: 'off',
595
+ renderWhitespace: 'none',
596
+ mouseWheelZoom: false
597
+ }}
598
+ />
599
+ {/key}
600
+ </div>
601
+
602
+ {#if hasChanges}
603
+ <div class="flex-shrink-0 p-4 bg-amber-50 dark:bg-amber-900/30 border-t border-amber-200 dark:border-amber-800">
604
+ <div class="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
605
+ <Icon name="lucide:circle-alert" class="w-3 h-3" />
606
+ Unsaved changes
607
+ </div>
608
+ </div>
609
+ {/if}
610
+ </div>
611
+ </div>
612
+ </div>
613
+ {/if}
614
+ {:else if isPdfFile(file.name)}
615
+ <!-- PDF preview -->
616
+ <div class="h-full w-full">
617
+ {#if pdfBlobUrl}
618
+ <iframe
619
+ src={pdfBlobUrl}
620
+ title={file.name}
621
+ class="w-full h-full border-0"
622
+ ></iframe>
623
+ {:else}
624
+ <div class="flex items-center justify-center h-full">
625
+ <LoadingSpinner size="lg" />
626
+ </div>
627
+ {/if}
628
+ </div>
629
+ {:else if isBinaryFile(file.name)}
630
+ <div class="flex flex-col items-center justify-center h-full p-8">
631
+ <Icon name="lucide:file-text" class="w-16 h-16 text-slate-400 mb-4" />
632
+ <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
633
+ Binary File
634
+ </h3>
635
+ <p class="text-sm text-slate-500 dark:text-slate-400 text-center mb-4">
636
+ This file cannot be previewed in the browser.
637
+ </p>
638
+ <button
639
+ class="px-6 py-2.5 bg-violet-600 text-white rounded-xl hover:bg-violet-700 transition-all duration-200"
640
+ onclick={downloadFile}
641
+ >
642
+ Download File
643
+ </button>
644
+ </div>
645
+ {:else}
646
+ <!-- Code content (always in edit mode) -->
647
+ <div class="h-full flex flex-col" bind:this={containerRef}>
648
+ <div class="flex-1 bg-slate-50 dark:bg-slate-950 overflow-hidden">
649
+ <div class="h-full flex flex-col">
650
+ <div class="flex-1">
651
+ {#key themeKey}
652
+ <MonacoEditor
653
+ bind:this={monacoEditorRef}
654
+ bind:value={editableContent}
655
+ language={getDetectedLanguage()}
656
+ readonly={false}
657
+ onChange={handleContentChange}
658
+ height={editorHeight}
659
+ options={{
660
+ minimap: { enabled: false },
661
+ wordWrap: wordWrap ? 'on' : 'off',
662
+ renderWhitespace: 'none',
663
+ mouseWheelZoom: false
664
+ }}
665
+ />
666
+ {/key}
667
+ </div>
668
+
669
+ </div>
670
+ </div>
671
+ </div>
672
+ {/if}
673
+ </div>
674
+ </div>
675
+ {:else}
676
+ <div class="h-full flex items-center justify-center">
677
+ <div class="text-center p-12">
678
+ <div class="bg-slate-100 dark:bg-slate-800 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
679
+ <Icon name="lucide:file-text" class="w-10 h-10 text-slate-400" />
680
+ </div>
681
+ <h3 class="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2">
682
+ No File Selected
683
+ </h3>
684
+ <p class="text-sm text-slate-600 dark:text-slate-400">
685
+ Select a file from the explorer to view its content.
686
+ </p>
687
+ </div>
688
+ </div>
689
+ {/if}
690
+
691
+ <style>
692
+ :global(.line-highlight) {
693
+ background-color: rgba(255, 235, 59, 0.3) !important;
694
+ animation: fade-out 3s ease-out forwards;
695
+ }
696
+
697
+ :global(.line-highlight-margin) {
698
+ background-color: rgba(255, 235, 59, 0.5) !important;
699
+ }
700
+
701
+ :global(.monaco-editor.vs-dark .line-highlight) {
702
+ background-color: rgba(255, 235, 59, 0.15) !important;
703
+ }
704
+
705
+ :global(.monaco-editor.vs-dark .line-highlight-margin) {
706
+ background-color: rgba(255, 235, 59, 0.25) !important;
707
+ }
708
+
709
+ @keyframes fade-out {
710
+ 0% {
711
+ background-color: rgba(255, 235, 59, 0.5);
712
+ }
713
+ 100% {
714
+ background-color: transparent;
715
+ }
716
+ }
717
+
718
+ :global(.monaco-editor.vs-dark) {
719
+ @keyframes fade-out {
720
+ 0% {
721
+ background-color: rgba(255, 235, 59, 0.15);
722
+ }
723
+ 100% {
724
+ background-color: transparent;
725
+ }
726
+ }
727
+ }
728
+ </style>