@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,1560 @@
1
+ <script lang="ts">
2
+ import { onMount, untrack } from 'svelte';
3
+ import Icon from '$frontend/lib/components/common/Icon.svelte';
4
+ import Dialog from '$frontend/lib/components/common/Dialog.svelte';
5
+ import { projectState } from '$frontend/lib/stores/core/projects.svelte';
6
+ import { showError, showInfo } from '$frontend/lib/stores/ui/notification.svelte';
7
+ import { debug } from '$shared/utils/logger';
8
+ import ws from '$frontend/lib/utils/ws';
9
+ import { getFileIcon } from '$frontend/lib/utils/file-icon-mappings';
10
+ import { getGitStatusLabel, getGitStatusColor } from '$frontend/lib/utils/git-status';
11
+ import type { IconName } from '$shared/types/ui/icons';
12
+ import type {
13
+ GitStatus,
14
+ GitBranchInfo,
15
+ GitFileChange,
16
+ GitFileDiff,
17
+ GitCommit,
18
+ GitConflictFile,
19
+ GitStashEntry,
20
+ GitTag,
21
+ GitRemote
22
+ } from '$shared/types/git';
23
+
24
+ // Sub-components
25
+ import CommitForm from '$frontend/lib/components/git/CommitForm.svelte';
26
+ import ChangesSection from '$frontend/lib/components/git/ChangesSection.svelte';
27
+ import DiffViewer from '$frontend/lib/components/git/DiffViewer.svelte';
28
+ import BranchManager from '$frontend/lib/components/git/BranchManager.svelte';
29
+ import GitLog from '$frontend/lib/components/git/GitLog.svelte';
30
+ import ConflictResolver from '$frontend/lib/components/git/ConflictResolver.svelte';
31
+
32
+ // Derived state
33
+ const hasActiveProject = $derived(projectState.currentProject !== null);
34
+ const projectId = $derived(projectState.currentProject?.id || '');
35
+
36
+ // Git state
37
+ let isRepo = $state(false);
38
+ let isLoading = $state(false);
39
+ let gitStatus = $state<GitStatus>({ staged: [], unstaged: [], untracked: [], conflicted: [] });
40
+ let branchInfo = $state<GitBranchInfo | null>(null);
41
+ let isCommitting = $state(false);
42
+
43
+ // Remote state
44
+ let remotes = $state<GitRemote[]>([]);
45
+ let selectedRemote = $state('origin');
46
+
47
+ // View state
48
+ let activeView = $state<'changes' | 'log' | 'stash' | 'tags'>('changes');
49
+ let viewMode = $state<'list' | 'diff'>('list');
50
+ let showBranchManager = $state(false);
51
+ let showConflictResolver = $state(false);
52
+
53
+ // Git init state
54
+ let isInitializing = $state(false);
55
+
56
+ // Stash state
57
+ let stashEntries = $state<GitStashEntry[]>([]);
58
+ let isStashLoading = $state(false);
59
+ let showStashSaveForm = $state(false);
60
+ let stashMessage = $state('');
61
+
62
+ // Tags state
63
+ let tags = $state<GitTag[]>([]);
64
+ let isTagsLoading = $state(false);
65
+ let showCreateTagForm = $state(false);
66
+ let newTagName = $state('');
67
+ let newTagMessage = $state('');
68
+
69
+ // More menu state (for Stash/Tags)
70
+ let showMoreMenu = $state(false);
71
+
72
+ // Tab system (like Files panel)
73
+ interface DiffTab {
74
+ id: string;
75
+ filePath: string;
76
+ fileName: string;
77
+ section: string;
78
+ diff: GitFileDiff | null;
79
+ diffs: GitFileDiff[];
80
+ isLoading: boolean;
81
+ commitHash?: string;
82
+ status?: string;
83
+ }
84
+
85
+ // Per-view tab isolation — each view (Changes, History, Stash, Tags) has its own tabs
86
+ const _tabStore: Record<string, DiffTab[]> = { changes: [], log: [], stash: [], tags: [] };
87
+ const _activeTabStore: Record<string, string | null> = { changes: null, log: null, stash: null, tags: null };
88
+ const _viewModeStore: Record<string, 'list' | 'diff'> = { changes: 'list', log: 'list', stash: 'list', tags: 'list' };
89
+
90
+ let openTabs = $state<DiffTab[]>([]);
91
+ let activeTabId = $state<string | null>(null);
92
+
93
+ const activeTab = $derived(openTabs.find(t => t.id === activeTabId) || null);
94
+
95
+ function switchToView(newView: typeof activeView) {
96
+ // Save current view's tab state
97
+ _tabStore[activeView] = openTabs;
98
+ _activeTabStore[activeView] = activeTabId;
99
+ _viewModeStore[activeView] = viewMode;
100
+ // Restore target view's tab state
101
+ openTabs = _tabStore[newView] || [];
102
+ activeTabId = _activeTabStore[newView] || null;
103
+ viewMode = _viewModeStore[newView] || 'list';
104
+ activeView = newView;
105
+ }
106
+
107
+ function resetAllViewTabs() {
108
+ for (const key of Object.keys(_tabStore)) {
109
+ _tabStore[key] = [];
110
+ _activeTabStore[key] = null;
111
+ _viewModeStore[key] = 'list';
112
+ }
113
+ openTabs = [];
114
+ activeTabId = null;
115
+ viewMode = 'list';
116
+ }
117
+
118
+ // Diff state
119
+ const isDiffLoading = $state(false);
120
+
121
+ // Log state
122
+ let commits = $state<GitCommit[]>([]);
123
+ let isLogLoading = $state(false);
124
+ let logHasMore = $state(false);
125
+ let logSkip = $state(0);
126
+
127
+ // Conflict state
128
+ let conflictFiles = $state<GitConflictFile[]>([]);
129
+ let isConflictLoading = $state(false);
130
+
131
+ // Container width for responsive layout (same threshold as Files: 800)
132
+ let containerRef = $state<HTMLDivElement | null>(null);
133
+ let containerWidth = $state(0);
134
+ const TWO_COLUMN_THRESHOLD = 800;
135
+ const isTwoColumnMode = $derived(containerWidth >= TWO_COLUMN_THRESHOLD);
136
+
137
+ // Track last project for re-fetch
138
+ let lastProjectId = $state('');
139
+
140
+ // (File watcher subscription managed by $effect with auto-cleanup)
141
+
142
+ // Active diff file path (for highlighting in list)
143
+ const activeFilePath = $derived(activeTab?.filePath || null);
144
+
145
+ // ============================
146
+ // Confirm Dialog State
147
+ // ============================
148
+ let showConfirmDialog = $state(false);
149
+ let confirmConfig = $state({
150
+ title: '',
151
+ message: '',
152
+ type: 'warning' as 'info' | 'warning' | 'error' | 'success',
153
+ confirmText: 'Confirm',
154
+ cancelText: 'Cancel',
155
+ onConfirm: () => {}
156
+ });
157
+
158
+ function requestConfirm(config: {
159
+ title: string;
160
+ message: string;
161
+ type?: 'info' | 'warning' | 'error' | 'success';
162
+ confirmText?: string;
163
+ cancelText?: string;
164
+ onConfirm: () => void;
165
+ }) {
166
+ confirmConfig = {
167
+ title: config.title,
168
+ message: config.message,
169
+ type: config.type || 'warning',
170
+ confirmText: config.confirmText || 'Confirm',
171
+ cancelText: config.cancelText || 'Cancel',
172
+ onConfirm: config.onConfirm
173
+ };
174
+ showConfirmDialog = true;
175
+ }
176
+
177
+ function closeConfirmDialog() {
178
+ showConfirmDialog = false;
179
+ }
180
+
181
+ // ============================
182
+ // Git Init
183
+ // ============================
184
+
185
+ async function handleInit() {
186
+ if (!projectId) return;
187
+ isInitializing = true;
188
+ try {
189
+ await ws.http('git:init', { projectId, defaultBranch: 'main' });
190
+ await loadAll();
191
+ } catch (err) {
192
+ debug.error('git', 'Git init failed:', err);
193
+ showError('Git Init Failed', err instanceof Error ? err.message : 'Unknown error');
194
+ } finally {
195
+ isInitializing = false;
196
+ }
197
+ }
198
+
199
+ // ============================
200
+ // Data Loading
201
+ // ============================
202
+
203
+ async function loadAll() {
204
+ if (!hasActiveProject || !projectId) return;
205
+ isLoading = true;
206
+ try {
207
+ await Promise.all([loadStatus(), loadBranches(), loadRemotes()]);
208
+ } catch (err) {
209
+ debug.error('git', 'Failed to load git data:', err);
210
+ } finally {
211
+ isLoading = false;
212
+ }
213
+ }
214
+
215
+ async function loadStatus() {
216
+ if (!projectId) return;
217
+ try {
218
+ const data = await ws.http('git:status', { projectId });
219
+ isRepo = data.isRepo;
220
+ if (data.isRepo) {
221
+ gitStatus = {
222
+ staged: data.staged,
223
+ unstaged: data.unstaged,
224
+ untracked: data.untracked,
225
+ conflicted: data.conflicted
226
+ };
227
+ }
228
+ } catch (err) {
229
+ debug.error('git', 'Failed to load status:', err);
230
+ }
231
+ }
232
+
233
+ async function loadBranches() {
234
+ if (!projectId) return;
235
+ try {
236
+ branchInfo = await ws.http('git:branches', { projectId });
237
+ } catch (err) {
238
+ debug.error('git', 'Failed to load branches:', err);
239
+ }
240
+ }
241
+
242
+ async function loadRemotes() {
243
+ if (!projectId) return;
244
+ try {
245
+ const list = await ws.http('git:remotes', { projectId });
246
+ remotes = list;
247
+ // Auto-select first remote if current selection doesn't exist
248
+ if (list.length > 0 && !list.find(r => r.name === selectedRemote)) {
249
+ selectedRemote = list[0].name;
250
+ }
251
+ } catch (err) {
252
+ debug.error('git', 'Failed to load remotes:', err);
253
+ }
254
+ }
255
+
256
+ async function loadLog(reset = false) {
257
+ if (!projectId) return;
258
+ isLogLoading = true;
259
+ try {
260
+ if (reset) {
261
+ logSkip = 0;
262
+ commits = [];
263
+ }
264
+ const data = await ws.http('git:log', { projectId, limit: 50, skip: logSkip });
265
+ if (reset) {
266
+ commits = data.commits;
267
+ } else {
268
+ commits = [...commits, ...data.commits];
269
+ }
270
+ logHasMore = data.hasMore;
271
+ logSkip += data.commits.length;
272
+ } catch (err) {
273
+ debug.error('git', 'Failed to load log:', err);
274
+ } finally {
275
+ isLogLoading = false;
276
+ }
277
+ }
278
+
279
+ async function loadConflicts() {
280
+ if (!projectId) return;
281
+ isConflictLoading = true;
282
+ try {
283
+ conflictFiles = await ws.http('git:conflict-files', { projectId });
284
+ } catch (err) {
285
+ debug.error('git', 'Failed to load conflicts:', err);
286
+ } finally {
287
+ isConflictLoading = false;
288
+ }
289
+ }
290
+
291
+ // ============================
292
+ // Staging Actions
293
+ // ============================
294
+
295
+ async function stageFile(path: string) {
296
+ if (!projectId) return;
297
+ try {
298
+ await ws.http('git:stage', { projectId, filePath: path });
299
+ await loadStatus();
300
+ } catch (err) {
301
+ debug.error('git', 'Failed to stage file:', err);
302
+ }
303
+ }
304
+
305
+ async function stageAll() {
306
+ if (!projectId) return;
307
+ try {
308
+ await ws.http('git:stage-all', { projectId });
309
+ await loadStatus();
310
+ } catch (err) {
311
+ debug.error('git', 'Failed to stage all:', err);
312
+ }
313
+ }
314
+
315
+ async function unstageFile(path: string) {
316
+ if (!projectId) return;
317
+ try {
318
+ await ws.http('git:unstage', { projectId, filePath: path });
319
+ await loadStatus();
320
+ } catch (err) {
321
+ debug.error('git', 'Failed to unstage file:', err);
322
+ }
323
+ }
324
+
325
+ async function unstageAll() {
326
+ if (!projectId) return;
327
+ try {
328
+ await ws.http('git:unstage-all', { projectId });
329
+ await loadStatus();
330
+ } catch (err) {
331
+ debug.error('git', 'Failed to unstage all:', err);
332
+ }
333
+ }
334
+
335
+ async function discardFile(path: string) {
336
+ const fileName = path.split(/[\\/]/).pop() || path;
337
+ requestConfirm({
338
+ title: 'Discard Changes',
339
+ message: `Discard changes to "${fileName}"? This cannot be undone.`,
340
+ type: 'error',
341
+ confirmText: 'Discard',
342
+ onConfirm: async () => {
343
+ if (!projectId) return;
344
+ try {
345
+ await ws.http('git:discard', { projectId, filePath: path });
346
+ await loadStatus();
347
+ } catch (err) {
348
+ debug.error('git', 'Failed to discard file:', err);
349
+ }
350
+ }
351
+ });
352
+ }
353
+
354
+ async function discardAll() {
355
+ requestConfirm({
356
+ title: 'Discard All Changes',
357
+ message: 'Discard ALL changes? This cannot be undone.',
358
+ type: 'error',
359
+ confirmText: 'Discard All',
360
+ onConfirm: async () => {
361
+ if (!projectId) return;
362
+ try {
363
+ await ws.http('git:discard-all', { projectId });
364
+ await loadStatus();
365
+ } catch (err) {
366
+ debug.error('git', 'Failed to discard all:', err);
367
+ }
368
+ }
369
+ });
370
+ }
371
+
372
+ // ============================
373
+ // Commit
374
+ // ============================
375
+
376
+ async function handleCommit(message: string) {
377
+ if (!projectId) return;
378
+ isCommitting = true;
379
+ try {
380
+ await ws.http('git:commit', { projectId, message });
381
+ await loadAll();
382
+ if (activeView === 'log') {
383
+ await loadLog(true);
384
+ }
385
+ } catch (err) {
386
+ debug.error('git', 'Commit failed:', err);
387
+ showError('Commit Failed', err instanceof Error ? err.message : 'Unknown error');
388
+ } finally {
389
+ isCommitting = false;
390
+ }
391
+ }
392
+
393
+ // ============================
394
+ // Tab Operations
395
+ // ============================
396
+
397
+ function selectTab(id: string) {
398
+ activeTabId = id;
399
+ }
400
+
401
+ // ============================
402
+ // Diff
403
+ // ============================
404
+
405
+ // Detect binary files by extension (for fallback when git diff returns empty)
406
+ function isBinaryByExtension(filePath: string): boolean {
407
+ const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
408
+ return [
409
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.svg',
410
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
411
+ '.zip', '.tar', '.gz', '.7z', '.rar', '.bz2',
412
+ '.exe', '.dll', '.so', '.dylib',
413
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
414
+ '.mp3', '.mp4', '.wav', '.avi', '.mkv', '.flv', '.mov', '.ogg',
415
+ '.sqlite', '.db',
416
+ ].includes(ext);
417
+ }
418
+
419
+ async function viewDiff(file: GitFileChange, section: string) {
420
+ if (!projectId) return;
421
+ const tabId = `${section}:${file.path}`;
422
+ const fileName = file.path.split(/[\\/]/).pop() || file.path;
423
+ const status = section === 'staged' ? file.indexStatus : file.workingStatus;
424
+
425
+ // Changes view: always replace with single tab
426
+ openTabs = [{
427
+ id: tabId,
428
+ filePath: file.path,
429
+ fileName,
430
+ section,
431
+ diff: null,
432
+ diffs: [],
433
+ isLoading: true,
434
+ status
435
+ }];
436
+ activeTabId = tabId;
437
+ if (!isTwoColumnMode) viewMode = 'diff';
438
+
439
+ try {
440
+ const action = section === 'staged' ? 'git:diff-staged' : 'git:diff-unstaged';
441
+ const diffs = await ws.http(action, { projectId, filePath: file.path });
442
+ let diffResult: GitFileDiff | null = diffs.length > 0 ? diffs[0] : null;
443
+
444
+ // When git diff returns empty (e.g. untracked files), create a synthetic diff
445
+ if (!diffResult) {
446
+ diffResult = {
447
+ oldPath: file.path,
448
+ newPath: file.path,
449
+ status: status || '?',
450
+ hunks: [],
451
+ isBinary: isBinaryByExtension(file.path)
452
+ };
453
+ } else if (status) {
454
+ // Override diff parser status with authoritative status from git status
455
+ // parseDiff defaults to 'M', but the real status (A, D, R, etc.) comes from git status
456
+ diffResult = { ...diffResult, status };
457
+ }
458
+
459
+ openTabs = openTabs.map(t =>
460
+ t.id === tabId ? { ...t, diff: diffResult, isLoading: false } : t
461
+ );
462
+ } catch (err) {
463
+ debug.error('git', 'Failed to load diff:', err);
464
+ openTabs = openTabs.map(t =>
465
+ t.id === tabId ? { ...t, diff: null, isLoading: false } : t
466
+ );
467
+ }
468
+ }
469
+
470
+ async function viewCommitDiff(hash: string) {
471
+ if (!projectId) return;
472
+ const loadingTabId = `commit:${hash}`;
473
+
474
+ // History view: show loading placeholder, then replace with per-file tabs
475
+ openTabs = [{
476
+ id: loadingTabId,
477
+ filePath: hash,
478
+ fileName: `Commit ${hash.substring(0, 7)}`,
479
+ section: 'commit',
480
+ diff: null,
481
+ diffs: [],
482
+ isLoading: true,
483
+ commitHash: hash
484
+ }];
485
+ activeTabId = loadingTabId;
486
+ if (!isTwoColumnMode) viewMode = 'diff';
487
+
488
+ try {
489
+ const diffs = await ws.http('git:diff-commit', { projectId, commitHash: hash });
490
+ if (diffs.length === 0) {
491
+ openTabs = [{
492
+ id: loadingTabId,
493
+ filePath: hash,
494
+ fileName: `Commit ${hash.substring(0, 7)}`,
495
+ section: 'commit',
496
+ diff: null,
497
+ diffs: [],
498
+ isLoading: false,
499
+ commitHash: hash
500
+ }];
501
+ return;
502
+ }
503
+
504
+ // Create one tab per file in the commit
505
+ const fileTabs: DiffTab[] = diffs.map((d: GitFileDiff, i: number) => {
506
+ const path = d.newPath || d.oldPath || `file-${i}`;
507
+ const name = path.split(/[\\/]/).pop() || path;
508
+ return {
509
+ id: `commit:${hash}:${path}`,
510
+ filePath: path,
511
+ fileName: name,
512
+ section: 'commit',
513
+ diff: d,
514
+ diffs: [],
515
+ isLoading: false,
516
+ commitHash: hash,
517
+ status: d.status
518
+ };
519
+ });
520
+ openTabs = fileTabs;
521
+ activeTabId = fileTabs[0].id;
522
+ } catch (err) {
523
+ debug.error('git', 'Failed to load commit diff:', err);
524
+ openTabs = [{
525
+ id: loadingTabId,
526
+ filePath: hash,
527
+ fileName: `Commit ${hash.substring(0, 7)}`,
528
+ section: 'commit',
529
+ diff: null,
530
+ diffs: [],
531
+ isLoading: false,
532
+ commitHash: hash
533
+ }];
534
+ }
535
+ }
536
+
537
+ // ============================
538
+ // Branch Operations
539
+ // ============================
540
+
541
+ async function switchBranch(name: string) {
542
+ if (!projectId) return;
543
+ try {
544
+ await ws.http('git:switch-branch', { projectId, name });
545
+ showBranchManager = false;
546
+ await loadAll();
547
+ } catch (err) {
548
+ debug.error('git', 'Failed to switch branch:', err);
549
+ showError('Switch Branch Failed', err instanceof Error ? err.message : 'Unknown error');
550
+ }
551
+ }
552
+
553
+ async function createBranch(name: string) {
554
+ if (!projectId) return;
555
+ try {
556
+ await ws.http('git:create-branch', { projectId, name });
557
+ showBranchManager = false;
558
+ await loadAll();
559
+ } catch (err) {
560
+ debug.error('git', 'Failed to create branch:', err);
561
+ showError('Create Branch Failed', err instanceof Error ? err.message : 'Unknown error');
562
+ }
563
+ }
564
+
565
+ async function deleteBranch(name: string) {
566
+ requestConfirm({
567
+ title: 'Delete Branch',
568
+ message: `Delete branch "${name}"?`,
569
+ type: 'error',
570
+ confirmText: 'Delete',
571
+ onConfirm: async () => {
572
+ if (!projectId) return;
573
+ try {
574
+ await ws.http('git:delete-branch', { projectId, name });
575
+ await loadBranches();
576
+ } catch (err) {
577
+ debug.error('git', 'Failed to delete branch:', err);
578
+ requestConfirm({
579
+ title: 'Force Delete Branch',
580
+ message: 'Branch is not fully merged. Force delete?',
581
+ type: 'error',
582
+ confirmText: 'Force Delete',
583
+ onConfirm: async () => {
584
+ try {
585
+ await ws.http('git:delete-branch', { projectId, name, force: true });
586
+ await loadBranches();
587
+ } catch (forceErr) {
588
+ showError('Force Delete Failed', forceErr instanceof Error ? forceErr.message : 'Unknown error');
589
+ }
590
+ }
591
+ });
592
+ }
593
+ }
594
+ });
595
+ }
596
+
597
+ async function renameBranch(oldName: string, newName: string) {
598
+ if (!projectId) return;
599
+ try {
600
+ await ws.http('git:rename-branch', { projectId, oldName, newName });
601
+ await loadBranches();
602
+ } catch (err) {
603
+ debug.error('git', 'Failed to rename branch:', err);
604
+ }
605
+ }
606
+
607
+ async function mergeBranch(name: string) {
608
+ requestConfirm({
609
+ title: 'Merge Branch',
610
+ message: `Merge "${name}" into "${branchInfo?.current}"?`,
611
+ type: 'info',
612
+ confirmText: 'Merge',
613
+ onConfirm: async () => {
614
+ if (!projectId) return;
615
+ try {
616
+ const result = await ws.http('git:merge-branch', { projectId, branchName: name });
617
+ showBranchManager = false;
618
+ if (!result.success) {
619
+ await loadAll();
620
+ if (gitStatus.conflicted.length > 0) {
621
+ await loadConflicts();
622
+ showConflictResolver = true;
623
+ } else {
624
+ showError('Merge Failed', result.message);
625
+ }
626
+ } else {
627
+ await loadAll();
628
+ }
629
+ } catch (err) {
630
+ debug.error('git', 'Failed to merge branch:', err);
631
+ }
632
+ }
633
+ });
634
+ }
635
+
636
+ // ============================
637
+ // Remote Operations
638
+ // ============================
639
+
640
+ let isFetching = $state(false);
641
+ let isPulling = $state(false);
642
+ let isPushing = $state(false);
643
+
644
+ async function handleFetch() {
645
+ if (!projectId || isFetching) return;
646
+ isFetching = true;
647
+ try {
648
+ const prevAhead = branchInfo?.ahead ?? 0;
649
+ const prevBehind = branchInfo?.behind ?? 0;
650
+ await ws.http('git:fetch', { projectId, remote: selectedRemote });
651
+ await loadBranches();
652
+ const newAhead = branchInfo?.ahead ?? 0;
653
+ const newBehind = branchInfo?.behind ?? 0;
654
+ const parts: string[] = [];
655
+ if (newAhead > 0) parts.push(`${newAhead} ahead`);
656
+ if (newBehind > 0) parts.push(`${newBehind} behind`);
657
+ if (parts.length > 0) {
658
+ showInfo('Fetch Complete', `Your branch is ${parts.join(', ')} ${selectedRemote}.`);
659
+ } else if (prevBehind > 0 || prevAhead > 0) {
660
+ showInfo('Fetch Complete', `In sync with ${selectedRemote}.`);
661
+ } else {
662
+ showInfo('Fetch Complete', `Already up to date with ${selectedRemote}.`);
663
+ }
664
+ } catch (err) {
665
+ debug.error('git', 'Fetch failed:', err);
666
+ showError('Fetch Failed', err instanceof Error ? err.message : 'Unknown error');
667
+ } finally {
668
+ isFetching = false;
669
+ }
670
+ }
671
+
672
+ async function handlePull() {
673
+ if (!projectId || isPulling) return;
674
+ isPulling = true;
675
+ try {
676
+ const prevBehind = branchInfo?.behind ?? 0;
677
+ const result = await ws.http('git:pull', { projectId, remote: selectedRemote });
678
+ if (!result.success) {
679
+ if (result.message.includes('conflict')) {
680
+ await loadAll();
681
+ await loadConflicts();
682
+ showConflictResolver = true;
683
+ } else {
684
+ showError('Pull Failed', result.message);
685
+ }
686
+ } else {
687
+ await loadAll();
688
+ if (prevBehind > 0) {
689
+ showInfo('Pull Complete', `Pulled ${prevBehind} commit${prevBehind > 1 ? 's' : ''} from ${selectedRemote}.`);
690
+ } else {
691
+ showInfo('Pull Complete', `Already up to date with ${selectedRemote}.`);
692
+ }
693
+ }
694
+ } catch (err) {
695
+ debug.error('git', 'Pull failed:', err);
696
+ showError('Pull Failed', err instanceof Error ? err.message : 'Unknown error');
697
+ } finally {
698
+ isPulling = false;
699
+ }
700
+ }
701
+
702
+ async function handlePush() {
703
+ if (!projectId || isPushing) return;
704
+ isPushing = true;
705
+ try {
706
+ const prevAhead = branchInfo?.ahead ?? 0;
707
+ const result = await ws.http('git:push', { projectId, remote: selectedRemote });
708
+ if (!result.success) {
709
+ showError('Push Failed', result.message);
710
+ } else {
711
+ await loadBranches();
712
+ if (prevAhead > 0) {
713
+ showInfo('Push Complete', `Pushed ${prevAhead} commit${prevAhead > 1 ? 's' : ''} to ${selectedRemote}.`);
714
+ } else {
715
+ showInfo('Push Complete', `Branch pushed to ${selectedRemote}.`);
716
+ }
717
+ }
718
+ } catch (err) {
719
+ debug.error('git', 'Push failed:', err);
720
+ showError('Push Failed', err instanceof Error ? err.message : 'Unknown error');
721
+ } finally {
722
+ isPushing = false;
723
+ }
724
+ }
725
+
726
+ // ============================
727
+ // Conflict Resolution
728
+ // ============================
729
+
730
+ async function resolveConflict(filePath: string, resolution: 'ours' | 'theirs' | 'custom', customContent?: string) {
731
+ if (!projectId) return;
732
+ try {
733
+ await ws.http('git:resolve-conflict', { projectId, filePath, resolution, customContent });
734
+ await loadConflicts();
735
+ await loadStatus();
736
+ if (conflictFiles.length === 0) {
737
+ showConflictResolver = false;
738
+ }
739
+ } catch (err) {
740
+ debug.error('git', 'Failed to resolve conflict:', err);
741
+ }
742
+ }
743
+
744
+ function resolveWithAI(filePath: string) {
745
+ showConflictResolver = false;
746
+ showInfo('AI Conflict Resolution', `To resolve "${filePath}" with AI, open the Chat panel and describe the conflict. The AI will help you resolve it.`);
747
+ }
748
+
749
+ async function abortMerge() {
750
+ requestConfirm({
751
+ title: 'Abort Merge',
752
+ message: 'Abort the current merge? All conflict resolutions will be lost.',
753
+ type: 'error',
754
+ confirmText: 'Abort Merge',
755
+ onConfirm: async () => {
756
+ if (!projectId) return;
757
+ try {
758
+ await ws.http('git:abort-merge', { projectId });
759
+ showConflictResolver = false;
760
+ await loadAll();
761
+ } catch (err) {
762
+ debug.error('git', 'Failed to abort merge:', err);
763
+ }
764
+ }
765
+ });
766
+ }
767
+
768
+ function openConflictResolver(path: string) {
769
+ loadConflicts().then(() => {
770
+ showConflictResolver = true;
771
+ });
772
+ }
773
+
774
+ // ============================
775
+ // Stash Operations
776
+ // ============================
777
+
778
+ async function loadStash() {
779
+ if (!projectId) return;
780
+ isStashLoading = true;
781
+ try {
782
+ stashEntries = await ws.http('git:stash-list', { projectId });
783
+ } catch (err) {
784
+ debug.error('git', 'Failed to load stash list:', err);
785
+ } finally {
786
+ isStashLoading = false;
787
+ }
788
+ }
789
+
790
+ async function handleStashSave() {
791
+ if (!projectId) return;
792
+ try {
793
+ await ws.http('git:stash-save', { projectId, message: stashMessage.trim() || undefined });
794
+ stashMessage = '';
795
+ showStashSaveForm = false;
796
+ await Promise.all([loadStash(), loadStatus()]);
797
+ } catch (err) {
798
+ debug.error('git', 'Stash save failed:', err);
799
+ showError('Stash Failed', err instanceof Error ? err.message : 'Unknown error');
800
+ }
801
+ }
802
+
803
+ async function handleStashPop(index: number) {
804
+ if (!projectId) return;
805
+ try {
806
+ await ws.http('git:stash-pop', { projectId, index });
807
+ await Promise.all([loadStash(), loadStatus()]);
808
+ } catch (err) {
809
+ debug.error('git', 'Stash pop failed:', err);
810
+ showError('Stash Pop Failed', err instanceof Error ? err.message : 'Unknown error');
811
+ }
812
+ }
813
+
814
+ async function handleStashDrop(index: number) {
815
+ requestConfirm({
816
+ title: 'Drop Stash',
817
+ message: `Drop stash@{${index}}? This cannot be undone.`,
818
+ type: 'error',
819
+ confirmText: 'Drop',
820
+ onConfirm: async () => {
821
+ if (!projectId) return;
822
+ try {
823
+ await ws.http('git:stash-drop', { projectId, index });
824
+ await loadStash();
825
+ } catch (err) {
826
+ debug.error('git', 'Stash drop failed:', err);
827
+ showError('Stash Drop Failed', err instanceof Error ? err.message : 'Unknown error');
828
+ }
829
+ }
830
+ });
831
+ }
832
+
833
+ // ============================
834
+ // Tag Operations
835
+ // ============================
836
+
837
+ async function loadTags() {
838
+ if (!projectId) return;
839
+ isTagsLoading = true;
840
+ try {
841
+ tags = await ws.http('git:tags', { projectId });
842
+ } catch (err) {
843
+ debug.error('git', 'Failed to load tags:', err);
844
+ } finally {
845
+ isTagsLoading = false;
846
+ }
847
+ }
848
+
849
+ async function handleCreateTag() {
850
+ if (!projectId || !newTagName.trim()) return;
851
+ try {
852
+ await ws.http('git:create-tag', {
853
+ projectId,
854
+ name: newTagName.trim(),
855
+ message: newTagMessage.trim() || undefined
856
+ });
857
+ newTagName = '';
858
+ newTagMessage = '';
859
+ showCreateTagForm = false;
860
+ await loadTags();
861
+ } catch (err) {
862
+ debug.error('git', 'Create tag failed:', err);
863
+ showError('Create Tag Failed', err instanceof Error ? err.message : 'Unknown error');
864
+ }
865
+ }
866
+
867
+ async function handleDeleteTag(name: string) {
868
+ requestConfirm({
869
+ title: 'Delete Tag',
870
+ message: `Delete tag "${name}"?`,
871
+ type: 'error',
872
+ confirmText: 'Delete',
873
+ onConfirm: async () => {
874
+ if (!projectId) return;
875
+ try {
876
+ await ws.http('git:delete-tag', { projectId, name });
877
+ await loadTags();
878
+ } catch (err) {
879
+ debug.error('git', 'Delete tag failed:', err);
880
+ showError('Delete Tag Failed', err instanceof Error ? err.message : 'Unknown error');
881
+ }
882
+ }
883
+ });
884
+ }
885
+
886
+ async function handlePushTag(name: string) {
887
+ if (!projectId) return;
888
+ try {
889
+ const result = await ws.http('git:push-tag', { projectId, name });
890
+ if (!result.success) {
891
+ showError('Push Tag Failed', result.message);
892
+ } else {
893
+ showInfo('Tag Pushed', `Tag "${name}" pushed to remote.`);
894
+ }
895
+ } catch (err) {
896
+ debug.error('git', 'Push tag failed:', err);
897
+ showError('Push Tag Failed', err instanceof Error ? err.message : 'Unknown error');
898
+ }
899
+ }
900
+
901
+ // ============================
902
+ // Lifecycle
903
+ // ============================
904
+
905
+ $effect(() => {
906
+ if (hasActiveProject && projectId) {
907
+ const prevId = untrack(() => lastProjectId);
908
+ if (projectId !== prevId) {
909
+ lastProjectId = projectId;
910
+ activeView = 'changes';
911
+ resetAllViewTabs();
912
+ commits = [];
913
+ logSkip = 0;
914
+ loadAll();
915
+ }
916
+ }
917
+ });
918
+
919
+ // Load log when switching to log view
920
+ $effect(() => {
921
+ if (activeView === 'log' && isRepo) {
922
+ untrack(() => {
923
+ if (commits.length === 0) {
924
+ loadLog(true);
925
+ }
926
+ });
927
+ }
928
+ });
929
+
930
+ // Load stash when switching to stash view
931
+ $effect(() => {
932
+ if (activeView === 'stash' && isRepo) {
933
+ untrack(() => loadStash());
934
+ }
935
+ });
936
+
937
+ // Load tags when switching to tags view
938
+ $effect(() => {
939
+ if (activeView === 'tags' && isRepo) {
940
+ untrack(() => loadTags());
941
+ }
942
+ });
943
+
944
+ // Sync view mode on column mode change
945
+ let prevTwoColumnMode = $state<boolean | null>(null);
946
+ $effect(() => {
947
+ if (prevTwoColumnMode !== null && prevTwoColumnMode !== isTwoColumnMode) {
948
+ if (!isTwoColumnMode) {
949
+ if (activeTabId && openTabs.length > 0) {
950
+ viewMode = 'diff';
951
+ } else {
952
+ viewMode = 'list';
953
+ }
954
+ }
955
+ }
956
+ prevTwoColumnMode = isTwoColumnMode;
957
+ });
958
+
959
+ // Start file watcher for this project (idempotent — won't duplicate if FilesPanel already started it)
960
+ $effect(() => {
961
+ if (hasActiveProject && projectId && projectState.currentProject?.path) {
962
+ ws.emit('files:watch', { projectPath: projectState.currentProject.path });
963
+ }
964
+ });
965
+
966
+ // Debounce timer for file/git change events
967
+ let changeDebounce: ReturnType<typeof setTimeout> | null = null;
968
+
969
+ // Shared refresh logic for both file changes and git state changes
970
+ function scheduleGitRefresh() {
971
+ if (changeDebounce) clearTimeout(changeDebounce);
972
+ changeDebounce = setTimeout(async () => {
973
+ changeDebounce = null;
974
+ // Refresh git status
975
+ await loadStatus();
976
+
977
+ // Refresh the active diff tab if currently viewing one
978
+ if (activeTab && !activeTab.isLoading && activeTab.section !== 'commit') {
979
+ const tab = activeTab;
980
+ try {
981
+ const action = tab.section === 'staged' ? 'git:diff-staged' : 'git:diff-unstaged';
982
+ const diffs = await ws.http(action, { projectId, filePath: tab.filePath });
983
+ const diffResult = diffs.length > 0 ? diffs[0] : null;
984
+ openTabs = openTabs.map(t =>
985
+ t.id === tab.id ? { ...t, diff: diffResult } : t
986
+ );
987
+ } catch {
988
+ // Silently ignore — file may have been removed from status
989
+ }
990
+ }
991
+ }, 400);
992
+ }
993
+
994
+ // Subscribe to file change events (working tree changes)
995
+ $effect(() => {
996
+ if (!hasActiveProject || !projectId) return;
997
+
998
+ const unsub = ws.on('files:changed', (payload: any) => {
999
+ if (payload.projectId !== projectId || !isRepo) return;
1000
+ scheduleGitRefresh();
1001
+ });
1002
+
1003
+ return () => {
1004
+ unsub();
1005
+ if (changeDebounce) {
1006
+ clearTimeout(changeDebounce);
1007
+ changeDebounce = null;
1008
+ }
1009
+ };
1010
+ });
1011
+
1012
+ // Subscribe to git state change events (external git add, commit, branch switch, etc.)
1013
+ $effect(() => {
1014
+ if (!hasActiveProject || !projectId) return;
1015
+
1016
+ const unsub = ws.on('git:changed', (payload: any) => {
1017
+ if (payload.projectId !== projectId || !isRepo) return;
1018
+ scheduleGitRefresh();
1019
+ // Also refresh branches in case of branch switch/create/delete
1020
+ loadBranches();
1021
+ // Refresh log if it was already loaded (History tab was visited)
1022
+ if (commits.length > 0) {
1023
+ loadLog(true);
1024
+ }
1025
+ });
1026
+
1027
+ return () => unsub();
1028
+ });
1029
+
1030
+ // Monitor container width
1031
+ onMount(() => {
1032
+ let resizeObserver: ResizeObserver | null = null;
1033
+ if (containerRef && typeof ResizeObserver !== 'undefined') {
1034
+ resizeObserver = new ResizeObserver((entries) => {
1035
+ for (const entry of entries) {
1036
+ containerWidth = entry.contentRect.width;
1037
+ }
1038
+ });
1039
+ resizeObserver.observe(containerRef);
1040
+ }
1041
+
1042
+ return () => {
1043
+ resizeObserver?.disconnect();
1044
+ };
1045
+ });
1046
+
1047
+ // Combined unstaged + untracked
1048
+ const allChanges = $derived([...gitStatus.unstaged, ...gitStatus.untracked]);
1049
+
1050
+ // Total changes count
1051
+ const totalChanges = $derived(
1052
+ gitStatus.staged.length + allChanges.length + gitStatus.conflicted.length
1053
+ );
1054
+
1055
+ // Exported panel actions for PanelHeader
1056
+ export const panelActions = {
1057
+ push: handlePush,
1058
+ pull: handlePull,
1059
+ fetch: handleFetch,
1060
+ init: handleInit,
1061
+ getBranchInfo: () => branchInfo,
1062
+ getIsRepo: () => isRepo,
1063
+ getIsFetching: () => isFetching,
1064
+ getIsPulling: () => isPulling,
1065
+ getIsPushing: () => isPushing,
1066
+ getRemotes: () => remotes,
1067
+ getHasRemotes: () => remotes.length > 0,
1068
+ getSelectedRemote: () => selectedRemote,
1069
+ setSelectedRemote: (name: string) => { selectedRemote = name; },
1070
+ setViewMode: (mode: 'list' | 'diff') => {
1071
+ if (!isTwoColumnMode) viewMode = mode;
1072
+ },
1073
+ getViewMode: () => viewMode,
1074
+ canShowDiff: () => openTabs.length > 0,
1075
+ isTwoColumnMode: () => isTwoColumnMode
1076
+ };
1077
+ </script>
1078
+
1079
+ <!-- Tab Bar Snippet -->
1080
+ {#snippet tabBar()}
1081
+ {#if openTabs.length > 0}
1082
+ <div class="flex items-center border-b border-slate-200 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-800/50 overflow-x-auto flex-shrink-0">
1083
+ {#each openTabs as tab (tab.id)}
1084
+ {@const isActive = tab.id === activeTabId}
1085
+ <div
1086
+ class="flex items-center gap-1.5 pl-3 pr-4 py-2 text-xs border-r border-slate-200/50 dark:border-slate-700/50 whitespace-nowrap transition-colors flex-shrink-0 cursor-pointer {isActive
1087
+ ? 'bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100'
1088
+ : 'text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-700 dark:hover:text-slate-300'}"
1089
+ onclick={() => selectTab(tab.id)}
1090
+ >
1091
+ <Icon name={getFileIcon(tab.fileName) as IconName} class="w-3.5 h-3.5 flex-shrink-0" />
1092
+ <span class="truncate max-w-28">{tab.fileName}</span>
1093
+ {#if tab.isLoading}
1094
+ <div class="w-2 h-2 border border-slate-400 border-t-transparent rounded-full animate-spin flex-shrink-0"></div>
1095
+ {:else if tab.status}
1096
+ <span class="text-xs font-bold {getGitStatusColor(tab.status)} flex-shrink-0">{getGitStatusLabel(tab.status)}</span>
1097
+ {/if}
1098
+ </div>
1099
+ {/each}
1100
+ </div>
1101
+ {/if}
1102
+ {/snippet}
1103
+
1104
+ <!-- Changes list snippet -->
1105
+ {#snippet changesList()}
1106
+ <!-- View tabs with branch switch -->
1107
+ <div class="flex items-center gap-1 px-2 py-1.5 border-b border-slate-100 dark:border-slate-800">
1108
+ <button
1109
+ type="button"
1110
+ class="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md bg-slate-100 dark:bg-slate-800/60 text-slate-700 dark:text-slate-300 hover:bg-violet-500/10 hover:text-violet-600 transition-colors cursor-pointer border-none min-w-0"
1111
+ onclick={() => showBranchManager = true}
1112
+ title="Switch Branch"
1113
+ >
1114
+ <Icon name="lucide:git-branch" class="w-3.5 h-3.5 shrink-0" />
1115
+ <span class="truncate max-w-24">{branchInfo?.current || '...'}</span>
1116
+ </button>
1117
+
1118
+ <div class="flex-1"></div>
1119
+
1120
+ <!-- Primary tabs: Changes & History -->
1121
+ <button
1122
+ type="button"
1123
+ class="px-2 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer border-none
1124
+ {activeView === 'changes'
1125
+ ? 'bg-violet-500/10 text-violet-600'
1126
+ : 'bg-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}"
1127
+ onclick={() => { switchToView('changes'); showMoreMenu = false; }}
1128
+ >
1129
+ Changes{totalChanges > 0 ? ` (${totalChanges})` : ''}
1130
+ </button>
1131
+ <button
1132
+ type="button"
1133
+ class="px-2 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer border-none
1134
+ {activeView === 'log'
1135
+ ? 'bg-violet-500/10 text-violet-600'
1136
+ : 'bg-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}"
1137
+ onclick={() => { switchToView('log'); showMoreMenu = false; }}
1138
+ >
1139
+ History
1140
+ </button>
1141
+
1142
+ <!-- More menu (Stash, Tags) -->
1143
+ <div class="relative">
1144
+ <button
1145
+ type="button"
1146
+ class="flex items-center justify-center w-7 h-7 rounded-md transition-colors cursor-pointer border-none
1147
+ {activeView === 'stash' || activeView === 'tags'
1148
+ ? 'bg-violet-500/10 text-violet-600'
1149
+ : 'bg-transparent text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800'}"
1150
+ onclick={() => showMoreMenu = !showMoreMenu}
1151
+ title="More views"
1152
+ >
1153
+ <Icon name="lucide:ellipsis" class="w-4 h-4" />
1154
+ </button>
1155
+
1156
+ {#if showMoreMenu}
1157
+ <div
1158
+ class="fixed inset-0 z-40"
1159
+ onclick={() => showMoreMenu = false}
1160
+ ></div>
1161
+ <div class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-lg py-1 min-w-32">
1162
+ <button
1163
+ type="button"
1164
+ class="flex items-center gap-2 w-full px-3 py-1.5 text-xs font-medium text-left transition-colors cursor-pointer border-none
1165
+ {activeView === 'stash'
1166
+ ? 'bg-violet-500/10 text-violet-600'
1167
+ : 'bg-transparent text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'}"
1168
+ onclick={() => { switchToView('stash'); showMoreMenu = false; }}
1169
+ >
1170
+ <Icon name="lucide:archive" class="w-3.5 h-3.5" />
1171
+ Stash
1172
+ {#if stashEntries.length > 0}
1173
+ <span class="ml-auto text-3xs font-semibold text-slate-400">{stashEntries.length}</span>
1174
+ {/if}
1175
+ </button>
1176
+ <button
1177
+ type="button"
1178
+ class="flex items-center gap-2 w-full px-3 py-1.5 text-xs font-medium text-left transition-colors cursor-pointer border-none
1179
+ {activeView === 'tags'
1180
+ ? 'bg-violet-500/10 text-violet-600'
1181
+ : 'bg-transparent text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'}"
1182
+ onclick={() => { switchToView('tags'); showMoreMenu = false; }}
1183
+ >
1184
+ <Icon name="lucide:tag" class="w-3.5 h-3.5" />
1185
+ Tags
1186
+ {#if tags.length > 0}
1187
+ <span class="ml-auto text-3xs font-semibold text-slate-400">{tags.length}</span>
1188
+ {/if}
1189
+ </button>
1190
+ </div>
1191
+ {/if}
1192
+ </div>
1193
+ </div>
1194
+
1195
+ {#if activeView === 'changes'}
1196
+ <!-- Commit form -->
1197
+ <CommitForm
1198
+ stagedCount={gitStatus.staged.length}
1199
+ {isCommitting}
1200
+ onCommit={handleCommit}
1201
+ />
1202
+
1203
+ <!-- Changes sections -->
1204
+ <div class="flex-1 overflow-y-auto px-1">
1205
+ {#if gitStatus.conflicted.length > 0}
1206
+ <ChangesSection
1207
+ title="Conflicts"
1208
+ icon="lucide:triangle-alert"
1209
+ files={gitStatus.conflicted}
1210
+ section="conflicted"
1211
+ onViewDiff={viewDiff}
1212
+ onResolve={openConflictResolver}
1213
+ />
1214
+ {/if}
1215
+
1216
+ <ChangesSection
1217
+ title="Staged Changes"
1218
+ icon="lucide:circle-check"
1219
+ files={gitStatus.staged}
1220
+ section="staged"
1221
+ onUnstage={unstageFile}
1222
+ onUnstageAll={unstageAll}
1223
+ onViewDiff={viewDiff}
1224
+ />
1225
+
1226
+ <ChangesSection
1227
+ title="Changes"
1228
+ icon="lucide:file-pen"
1229
+ files={allChanges}
1230
+ section="unstaged"
1231
+ onStage={stageFile}
1232
+ onStageAll={stageAll}
1233
+ onDiscard={discardFile}
1234
+ onDiscardAll={discardAll}
1235
+ onViewDiff={viewDiff}
1236
+ />
1237
+
1238
+ {#if totalChanges === 0 && !isLoading}
1239
+ <div class="flex flex-col items-center justify-center gap-2 py-8 text-slate-500 text-xs">
1240
+ <Icon name="lucide:circle-check" class="w-6 h-6 opacity-30" />
1241
+ <span>Working tree clean</span>
1242
+ </div>
1243
+ {/if}
1244
+ </div>
1245
+ {:else if activeView === 'log'}
1246
+ <GitLog
1247
+ {commits}
1248
+ isLoading={isLogLoading}
1249
+ hasMore={logHasMore}
1250
+ onLoadMore={() => loadLog()}
1251
+ onViewCommit={viewCommitDiff}
1252
+ />
1253
+ {:else if activeView === 'stash'}
1254
+ <!-- Stash View -->
1255
+ <div class="flex-1 overflow-y-auto">
1256
+ <!-- Stash save button/form -->
1257
+ <div class="px-2 pb-2">
1258
+ {#if showStashSaveForm}
1259
+ <div class="p-2.5 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg space-y-2">
1260
+ <input
1261
+ type="text"
1262
+ bind:value={stashMessage}
1263
+ placeholder="Stash message (optional)..."
1264
+ class="w-full px-2.5 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40 focus:ring-1 focus:ring-violet-500/20"
1265
+ onkeydown={(e) => e.key === 'Enter' && handleStashSave()}
1266
+ />
1267
+ <div class="flex gap-1.5">
1268
+ <button
1269
+ type="button"
1270
+ class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md bg-violet-600 text-white hover:bg-violet-700 transition-colors cursor-pointer border-none"
1271
+ onclick={handleStashSave}
1272
+ >
1273
+ Stash Changes
1274
+ </button>
1275
+ <button
1276
+ type="button"
1277
+ class="px-3 py-1.5 text-xs font-medium bg-transparent border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
1278
+ onclick={() => { showStashSaveForm = false; stashMessage = ''; }}
1279
+ >
1280
+ Cancel
1281
+ </button>
1282
+ </div>
1283
+ </div>
1284
+ {:else}
1285
+ <button
1286
+ type="button"
1287
+ class="flex items-center justify-center gap-2 w-full py-2 px-3 border border-dashed border-slate-300 dark:border-slate-600 rounded-lg text-xs text-slate-500 hover:text-violet-600 hover:border-violet-400 transition-colors cursor-pointer bg-transparent"
1288
+ onclick={() => showStashSaveForm = true}
1289
+ >
1290
+ <Icon name="lucide:archive" class="w-3.5 h-3.5" />
1291
+ <span>Stash Current Changes</span>
1292
+ </button>
1293
+ {/if}
1294
+ </div>
1295
+
1296
+ {#if isStashLoading}
1297
+ <div class="flex items-center justify-center py-8">
1298
+ <div class="w-5 h-5 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
1299
+ </div>
1300
+ {:else if stashEntries.length === 0}
1301
+ <div class="flex flex-col items-center justify-center gap-2 py-8 text-slate-500 text-xs">
1302
+ <Icon name="lucide:archive" class="w-6 h-6 opacity-30" />
1303
+ <span>No stashed changes</span>
1304
+ </div>
1305
+ {:else}
1306
+ <div class="space-y-1 px-1">
1307
+ {#each stashEntries as entry (entry.index)}
1308
+ <div class="group flex items-center gap-2 px-2.5 py-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors">
1309
+ <Icon name="lucide:archive" class="w-4 h-4 text-slate-400 shrink-0" />
1310
+ <div class="flex-1 min-w-0">
1311
+ <p class="text-xs font-medium text-slate-900 dark:text-slate-100 truncate">{entry.message}</p>
1312
+ <p class="text-3xs text-slate-400 dark:text-slate-500">stash@&#123;{entry.index}&#125;</p>
1313
+ </div>
1314
+ <div class="flex items-center gap-0.5 shrink-0">
1315
+ <button
1316
+ type="button"
1317
+ class="flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:bg-emerald-500/10 hover:text-emerald-500 transition-colors bg-transparent border-none cursor-pointer"
1318
+ onclick={() => handleStashPop(entry.index)}
1319
+ title="Pop (apply and remove)"
1320
+ >
1321
+ <Icon name="lucide:archive-restore" class="w-3.5 h-3.5" />
1322
+ </button>
1323
+ <button
1324
+ type="button"
1325
+ class="flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:bg-red-500/10 hover:text-red-500 transition-colors bg-transparent border-none cursor-pointer"
1326
+ onclick={() => handleStashDrop(entry.index)}
1327
+ title="Drop (delete)"
1328
+ >
1329
+ <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
1330
+ </button>
1331
+ </div>
1332
+ </div>
1333
+ {/each}
1334
+ </div>
1335
+ {/if}
1336
+ </div>
1337
+ {:else if activeView === 'tags'}
1338
+ <!-- Tags View -->
1339
+ <div class="flex-1 overflow-y-auto">
1340
+ <!-- Create tag button/form -->
1341
+ <div class="px-2 pb-2">
1342
+ {#if showCreateTagForm}
1343
+ <div class="p-2.5 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg space-y-2">
1344
+ <input
1345
+ type="text"
1346
+ bind:value={newTagName}
1347
+ placeholder="Tag name (e.g. v1.0.0)..."
1348
+ class="w-full px-2.5 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40 focus:ring-1 focus:ring-violet-500/20"
1349
+ onkeydown={(e) => e.key === 'Enter' && !newTagMessage && handleCreateTag()}
1350
+ />
1351
+ <input
1352
+ type="text"
1353
+ bind:value={newTagMessage}
1354
+ placeholder="Tag message (optional, makes annotated tag)..."
1355
+ class="w-full px-2.5 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40 focus:ring-1 focus:ring-violet-500/20"
1356
+ onkeydown={(e) => e.key === 'Enter' && handleCreateTag()}
1357
+ />
1358
+ <div class="flex gap-1.5">
1359
+ <button
1360
+ type="button"
1361
+ class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors cursor-pointer border-none
1362
+ {newTagName.trim()
1363
+ ? 'bg-violet-600 text-white hover:bg-violet-700'
1364
+ : 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500 cursor-not-allowed'}"
1365
+ onclick={handleCreateTag}
1366
+ disabled={!newTagName.trim()}
1367
+ >
1368
+ Create Tag
1369
+ </button>
1370
+ <button
1371
+ type="button"
1372
+ class="px-3 py-1.5 text-xs font-medium bg-transparent border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
1373
+ onclick={() => { showCreateTagForm = false; newTagName = ''; newTagMessage = ''; }}
1374
+ >
1375
+ Cancel
1376
+ </button>
1377
+ </div>
1378
+ </div>
1379
+ {:else}
1380
+ <button
1381
+ type="button"
1382
+ class="flex items-center justify-center gap-2 w-full py-2 px-3 border border-dashed border-slate-300 dark:border-slate-600 rounded-lg text-xs text-slate-500 hover:text-violet-600 hover:border-violet-400 transition-colors cursor-pointer bg-transparent"
1383
+ onclick={() => showCreateTagForm = true}
1384
+ >
1385
+ <Icon name="lucide:tag" class="w-3.5 h-3.5" />
1386
+ <span>Create New Tag</span>
1387
+ </button>
1388
+ {/if}
1389
+ </div>
1390
+
1391
+ {#if isTagsLoading}
1392
+ <div class="flex items-center justify-center py-8">
1393
+ <div class="w-5 h-5 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
1394
+ </div>
1395
+ {:else if tags.length === 0}
1396
+ <div class="flex flex-col items-center justify-center gap-2 py-8 text-slate-500 text-xs">
1397
+ <Icon name="lucide:tag" class="w-6 h-6 opacity-30" />
1398
+ <span>No tags</span>
1399
+ </div>
1400
+ {:else}
1401
+ <div class="space-y-1 px-1">
1402
+ {#each tags as tag (tag.name)}
1403
+ <div class="group flex items-center gap-2 px-2.5 py-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors">
1404
+ <Icon
1405
+ name={tag.isAnnotated ? 'lucide:bookmark' : 'lucide:tag'}
1406
+ class="w-4 h-4 shrink-0 {tag.isAnnotated ? 'text-amber-500' : 'text-slate-400'}"
1407
+ />
1408
+ <div class="flex-1 min-w-0">
1409
+ <div class="flex items-center gap-1.5">
1410
+ <p class="text-xs font-medium text-slate-900 dark:text-slate-100 truncate">{tag.name}</p>
1411
+ {#if tag.isAnnotated}
1412
+ <span class="text-3xs px-1 py-0.5 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 shrink-0">annotated</span>
1413
+ {/if}
1414
+ </div>
1415
+ {#if tag.message}
1416
+ <p class="text-3xs text-slate-500 dark:text-slate-400 truncate">{tag.message}</p>
1417
+ {/if}
1418
+ <p class="text-3xs text-slate-400 dark:text-slate-500 font-mono">{tag.hash}</p>
1419
+ </div>
1420
+ <div class="flex items-center gap-0.5 shrink-0">
1421
+ <button
1422
+ type="button"
1423
+ class="flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:bg-blue-500/10 hover:text-blue-500 transition-colors bg-transparent border-none cursor-pointer"
1424
+ onclick={() => handlePushTag(tag.name)}
1425
+ title="Push tag to remote"
1426
+ >
1427
+ <Icon name="lucide:arrow-up-from-line" class="w-3.5 h-3.5" />
1428
+ </button>
1429
+ <button
1430
+ type="button"
1431
+ class="flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:bg-red-500/10 hover:text-red-500 transition-colors bg-transparent border-none cursor-pointer"
1432
+ onclick={() => handleDeleteTag(tag.name)}
1433
+ title="Delete tag"
1434
+ >
1435
+ <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
1436
+ </button>
1437
+ </div>
1438
+ </div>
1439
+ {/each}
1440
+ </div>
1441
+ {/if}
1442
+ </div>
1443
+ {/if}
1444
+ {/snippet}
1445
+
1446
+ <!-- Diff panel snippet -->
1447
+ {#snippet diffPanel()}
1448
+ {@render tabBar()}
1449
+ <div class="flex-1 overflow-hidden">
1450
+ {#if activeTab}
1451
+ <DiffViewer
1452
+ diff={activeTab.diff}
1453
+ isLoading={activeTab.isLoading}
1454
+ />
1455
+ {:else}
1456
+ <div class="h-full flex flex-col items-center justify-center gap-2 text-slate-500 text-xs">
1457
+ <Icon name="lucide:file-diff" class="w-8 h-8 opacity-30" />
1458
+ <span>Select a file to view diff</span>
1459
+ </div>
1460
+ {/if}
1461
+ </div>
1462
+ {/snippet}
1463
+
1464
+ <div class="h-full flex flex-col bg-transparent" bind:this={containerRef}>
1465
+ {#if !hasActiveProject}
1466
+ <div class="flex-1 flex flex-col items-center justify-center gap-3 text-slate-600 dark:text-slate-500 text-sm">
1467
+ <Icon name="lucide:git-branch" class="w-10 h-10 opacity-30" />
1468
+ <span>No project selected</span>
1469
+ </div>
1470
+ {:else if isLoading && !isRepo}
1471
+ <div class="flex-1 flex flex-col items-center justify-center gap-3 text-slate-600 dark:text-slate-500 text-sm">
1472
+ <div class="w-6 h-6 border-2 border-slate-200 dark:border-slate-800 border-t-violet-600 rounded-full animate-spin"></div>
1473
+ <span>Loading...</span>
1474
+ </div>
1475
+ {:else if !isRepo}
1476
+ <div class="flex-1 flex flex-col items-center justify-center gap-4 text-slate-600 dark:text-slate-500 text-sm px-6">
1477
+ <Icon name="lucide:git-branch" class="w-10 h-10 opacity-30" />
1478
+ <span>Not a git repository</span>
1479
+ <p class="text-xs text-slate-400 dark:text-slate-500 text-center max-w-60">
1480
+ Initialize a git repository to start tracking your changes.
1481
+ </p>
1482
+ <button
1483
+ type="button"
1484
+ class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150
1485
+ {isInitializing
1486
+ ? 'bg-slate-100 dark:bg-slate-800 text-slate-400 cursor-not-allowed'
1487
+ : 'bg-violet-600 text-white hover:bg-violet-700 cursor-pointer'}"
1488
+ onclick={handleInit}
1489
+ disabled={isInitializing}
1490
+ >
1491
+ {#if isInitializing}
1492
+ <div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
1493
+ <span>Initializing...</span>
1494
+ {:else}
1495
+ <Icon name="lucide:folder-git-2" class="w-4 h-4" />
1496
+ <span>Initialize Repository</span>
1497
+ {/if}
1498
+ </button>
1499
+ </div>
1500
+ {:else}
1501
+ <div class="flex-1 overflow-hidden">
1502
+ <!-- Unified layout: always render both panels to preserve state (like Files panel) -->
1503
+ <div class="h-full flex">
1504
+ <!-- Left panel: Changes list (w-80 like Files panel tree) -->
1505
+ <div
1506
+ class={isTwoColumnMode
1507
+ ? 'w-80 flex-shrink-0 h-full overflow-hidden border-r border-slate-200 dark:border-slate-700 flex flex-col'
1508
+ : (viewMode === 'list' ? 'w-full h-full overflow-hidden flex flex-col' : 'hidden')}
1509
+ >
1510
+ {@render changesList()}
1511
+ </div>
1512
+
1513
+ <!-- Right panel: Diff viewer (like Files panel editor) -->
1514
+ <div
1515
+ class={isTwoColumnMode
1516
+ ? 'flex-1 h-full overflow-hidden flex flex-col'
1517
+ : (viewMode === 'diff' ? 'w-full h-full flex flex-col' : 'hidden')}
1518
+ >
1519
+ {@render diffPanel()}
1520
+ </div>
1521
+ </div>
1522
+ </div>
1523
+ {/if}
1524
+
1525
+ <!-- Branch Manager Modal -->
1526
+ <BranchManager
1527
+ isOpen={showBranchManager}
1528
+ {branchInfo}
1529
+ onClose={() => showBranchManager = false}
1530
+ onSwitch={switchBranch}
1531
+ onCreate={createBranch}
1532
+ onDelete={deleteBranch}
1533
+ onRename={renameBranch}
1534
+ onMerge={mergeBranch}
1535
+ onRemotesChanged={loadRemotes}
1536
+ />
1537
+
1538
+ <!-- Conflict Resolver Modal -->
1539
+ <ConflictResolver
1540
+ isOpen={showConflictResolver}
1541
+ {conflictFiles}
1542
+ isLoading={isConflictLoading}
1543
+ onResolve={resolveConflict}
1544
+ onResolveWithAI={resolveWithAI}
1545
+ onAbortMerge={abortMerge}
1546
+ onClose={() => showConflictResolver = false}
1547
+ />
1548
+
1549
+ <!-- Confirm Dialog -->
1550
+ <Dialog
1551
+ bind:isOpen={showConfirmDialog}
1552
+ onClose={closeConfirmDialog}
1553
+ type={confirmConfig.type}
1554
+ title={confirmConfig.title}
1555
+ message={confirmConfig.message}
1556
+ confirmText={confirmConfig.confirmText}
1557
+ cancelText={confirmConfig.cancelText}
1558
+ onConfirm={confirmConfig.onConfirm}
1559
+ />
1560
+ </div>