@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.
- package/.env.example +6 -0
- package/.github/workflows/release.yml +60 -0
- package/.github/workflows/test.yml +40 -0
- package/CONTRIBUTING.md +499 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/backend/index.ts +156 -0
- package/backend/lib/chat/helpers.ts +42 -0
- package/backend/lib/chat/index.ts +2 -0
- package/backend/lib/chat/stream-manager.ts +1126 -0
- package/backend/lib/database/README.md +77 -0
- package/backend/lib/database/index.ts +119 -0
- package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
- package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
- package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
- package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
- package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
- package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
- package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
- package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
- package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
- package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
- package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
- package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
- package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
- package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
- package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
- package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
- package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
- package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
- package/backend/lib/database/migrations/index.ts +154 -0
- package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
- package/backend/lib/database/queries/engine-queries.ts +75 -0
- package/backend/lib/database/queries/index.ts +9 -0
- package/backend/lib/database/queries/message-queries.ts +472 -0
- package/backend/lib/database/queries/project-queries.ts +117 -0
- package/backend/lib/database/queries/session-queries.ts +271 -0
- package/backend/lib/database/queries/settings-queries.ts +34 -0
- package/backend/lib/database/queries/snapshot-queries.ts +326 -0
- package/backend/lib/database/queries/utils-queries.ts +59 -0
- package/backend/lib/database/seeders/index.ts +13 -0
- package/backend/lib/database/seeders/settings_seeder.ts +84 -0
- package/backend/lib/database/utils/connection.ts +174 -0
- package/backend/lib/database/utils/index.ts +4 -0
- package/backend/lib/database/utils/migration-runner.ts +118 -0
- package/backend/lib/database/utils/seeder-runner.ts +121 -0
- package/backend/lib/engine/adapters/claude/environment.ts +164 -0
- package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
- package/backend/lib/engine/adapters/claude/index.ts +1 -0
- package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
- package/backend/lib/engine/adapters/claude/stream.ts +177 -0
- package/backend/lib/engine/adapters/opencode/index.ts +2 -0
- package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
- package/backend/lib/engine/adapters/opencode/server.ts +104 -0
- package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
- package/backend/lib/engine/index.ts +196 -0
- package/backend/lib/engine/types.ts +58 -0
- package/backend/lib/files/file-operations.ts +478 -0
- package/backend/lib/files/file-reading.ts +308 -0
- package/backend/lib/files/file-watcher.ts +383 -0
- package/backend/lib/files/path-browsing.ts +382 -0
- package/backend/lib/git/git-executor.ts +88 -0
- package/backend/lib/git/git-parser.ts +411 -0
- package/backend/lib/git/git-service.ts +505 -0
- package/backend/lib/mcp/README.md +1144 -0
- package/backend/lib/mcp/config.ts +316 -0
- package/backend/lib/mcp/index.ts +35 -0
- package/backend/lib/mcp/project-context.ts +236 -0
- package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
- package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
- package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
- package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
- package/backend/lib/mcp/servers/helper.ts +143 -0
- package/backend/lib/mcp/servers/index.ts +45 -0
- package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
- package/backend/lib/mcp/servers/weather/index.ts +31 -0
- package/backend/lib/mcp/stdio-server.ts +103 -0
- package/backend/lib/mcp/types.ts +65 -0
- package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
- package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
- package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
- package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
- package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
- package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
- package/backend/lib/preview/browser/browser-pool.ts +357 -0
- package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
- package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
- package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
- package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
- package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
- package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
- package/backend/lib/preview/browser/types.ts +359 -0
- package/backend/lib/preview/index.ts +24 -0
- package/backend/lib/project/index.ts +2 -0
- package/backend/lib/project/status-manager.ts +182 -0
- package/backend/lib/shared/index.ts +2 -0
- package/backend/lib/shared/port-utils.ts +25 -0
- package/backend/lib/shared/process-manager.ts +281 -0
- package/backend/lib/snapshot/blob-store.ts +227 -0
- package/backend/lib/snapshot/gitignore.ts +307 -0
- package/backend/lib/snapshot/helpers.ts +397 -0
- package/backend/lib/snapshot/snapshot-service.ts +483 -0
- package/backend/lib/terminal/helpers.ts +14 -0
- package/backend/lib/terminal/index.ts +8 -0
- package/backend/lib/terminal/pty-manager.ts +4 -0
- package/backend/lib/terminal/pty-session-manager.ts +387 -0
- package/backend/lib/terminal/shell-utils.ts +313 -0
- package/backend/lib/terminal/stream-manager.ts +293 -0
- package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
- package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
- package/backend/lib/user/helpers.ts +87 -0
- package/backend/lib/utils/ws.ts +944 -0
- package/backend/lib/vite-dev.ts +353 -0
- package/backend/middleware/cors.ts +15 -0
- package/backend/middleware/error-handler.ts +49 -0
- package/backend/middleware/logger.ts +9 -0
- package/backend/types/api.ts +24 -0
- package/backend/ws/README.md +1505 -0
- package/backend/ws/chat/background.ts +198 -0
- package/backend/ws/chat/index.ts +21 -0
- package/backend/ws/chat/stream.ts +707 -0
- package/backend/ws/engine/claude/accounts.ts +401 -0
- package/backend/ws/engine/claude/index.ts +13 -0
- package/backend/ws/engine/claude/status.ts +43 -0
- package/backend/ws/engine/index.ts +14 -0
- package/backend/ws/engine/opencode/index.ts +11 -0
- package/backend/ws/engine/opencode/status.ts +30 -0
- package/backend/ws/engine/utils.ts +36 -0
- package/backend/ws/files/index.ts +30 -0
- package/backend/ws/files/read.ts +189 -0
- package/backend/ws/files/search.ts +453 -0
- package/backend/ws/files/watch.ts +124 -0
- package/backend/ws/files/write.ts +143 -0
- package/backend/ws/git/branch.ts +106 -0
- package/backend/ws/git/commit.ts +39 -0
- package/backend/ws/git/conflict.ts +68 -0
- package/backend/ws/git/diff.ts +69 -0
- package/backend/ws/git/index.ts +24 -0
- package/backend/ws/git/log.ts +41 -0
- package/backend/ws/git/remote.ts +214 -0
- package/backend/ws/git/staging.ts +84 -0
- package/backend/ws/git/status.ts +90 -0
- package/backend/ws/index.ts +69 -0
- package/backend/ws/mcp/index.ts +61 -0
- package/backend/ws/messages/crud.ts +74 -0
- package/backend/ws/messages/index.ts +14 -0
- package/backend/ws/preview/browser/cleanup.ts +129 -0
- package/backend/ws/preview/browser/console.ts +114 -0
- package/backend/ws/preview/browser/interact.ts +513 -0
- package/backend/ws/preview/browser/mcp.ts +129 -0
- package/backend/ws/preview/browser/native-ui.ts +235 -0
- package/backend/ws/preview/browser/stats.ts +55 -0
- package/backend/ws/preview/browser/tab-info.ts +126 -0
- package/backend/ws/preview/browser/tab.ts +166 -0
- package/backend/ws/preview/browser/webcodecs.ts +293 -0
- package/backend/ws/preview/index.ts +146 -0
- package/backend/ws/projects/crud.ts +113 -0
- package/backend/ws/projects/index.ts +25 -0
- package/backend/ws/projects/presence.ts +46 -0
- package/backend/ws/projects/status.ts +116 -0
- package/backend/ws/sessions/crud.ts +327 -0
- package/backend/ws/sessions/index.ts +33 -0
- package/backend/ws/settings/crud.ts +112 -0
- package/backend/ws/settings/index.ts +14 -0
- package/backend/ws/snapshot/index.ts +17 -0
- package/backend/ws/snapshot/restore.ts +173 -0
- package/backend/ws/snapshot/timeline.ts +141 -0
- package/backend/ws/system/index.ts +14 -0
- package/backend/ws/system/operations.ts +49 -0
- package/backend/ws/terminal/index.ts +40 -0
- package/backend/ws/terminal/persistence.ts +153 -0
- package/backend/ws/terminal/session.ts +382 -0
- package/backend/ws/terminal/stream.ts +79 -0
- package/backend/ws/tunnel/index.ts +14 -0
- package/backend/ws/tunnel/operations.ts +91 -0
- package/backend/ws/types.ts +20 -0
- package/backend/ws/user/crud.ts +156 -0
- package/backend/ws/user/index.ts +14 -0
- package/bin/clopen.ts +307 -0
- package/bun.lock +1352 -0
- package/frontend/App.svelte +34 -0
- package/frontend/app.css +313 -0
- package/frontend/lib/app-environment.ts +10 -0
- package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
- package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
- package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
- package/frontend/lib/components/chat/formatters/index.ts +3 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
- package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
- package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
- package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
- package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
- package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
- package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
- package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
- package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
- package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
- package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
- package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
- package/frontend/lib/components/chat/shared/index.ts +2 -0
- package/frontend/lib/components/chat/shared/utils.ts +116 -0
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
- package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
- package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
- package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
- package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
- package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
- package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
- package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
- package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
- package/frontend/lib/components/chat/tools/components/index.ts +7 -0
- package/frontend/lib/components/chat/tools/index.ts +26 -0
- package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
- package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
- package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
- package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
- package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
- package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
- package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
- package/frontend/lib/components/common/Alert.svelte +139 -0
- package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
- package/frontend/lib/components/common/Button.svelte +71 -0
- package/frontend/lib/components/common/Card.svelte +102 -0
- package/frontend/lib/components/common/Checkbox.svelte +48 -0
- package/frontend/lib/components/common/Dialog.svelte +249 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
- package/frontend/lib/components/common/Icon.svelte +58 -0
- package/frontend/lib/components/common/Input.svelte +72 -0
- package/frontend/lib/components/common/Lightbox.svelte +233 -0
- package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
- package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
- package/frontend/lib/components/common/Modal.svelte +177 -0
- package/frontend/lib/components/common/ModalProvider.svelte +28 -0
- package/frontend/lib/components/common/ModelSelector.svelte +110 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
- package/frontend/lib/components/common/NotificationToast.svelte +113 -0
- package/frontend/lib/components/common/PageTemplate.svelte +76 -0
- package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
- package/frontend/lib/components/common/Select.svelte +98 -0
- package/frontend/lib/components/common/Textarea.svelte +80 -0
- package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
- package/frontend/lib/components/common/lucide-icons.ts +1642 -0
- package/frontend/lib/components/common/material-icons.ts +1082 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
- package/frontend/lib/components/common/xterm/index.ts +16 -0
- package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
- package/frontend/lib/components/common/xterm/types.ts +31 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
- package/frontend/lib/components/files/FileNode.svelte +384 -0
- package/frontend/lib/components/files/FileTree.svelte +681 -0
- package/frontend/lib/components/files/FileViewer.svelte +728 -0
- package/frontend/lib/components/files/SearchResults.svelte +303 -0
- package/frontend/lib/components/git/BranchManager.svelte +458 -0
- package/frontend/lib/components/git/ChangesSection.svelte +107 -0
- package/frontend/lib/components/git/CommitForm.svelte +76 -0
- package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
- package/frontend/lib/components/git/DiffViewer.svelte +364 -0
- package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
- package/frontend/lib/components/git/GitButton.svelte +33 -0
- package/frontend/lib/components/git/GitLog.svelte +361 -0
- package/frontend/lib/components/git/GitModal.svelte +80 -0
- package/frontend/lib/components/history/HistoryModal.svelte +563 -0
- package/frontend/lib/components/history/HistoryView.svelte +615 -0
- package/frontend/lib/components/index.ts +34 -0
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
- package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
- package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
- package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
- package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
- package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
- package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
- package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
- package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
- package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
- package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
- package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
- package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
- package/frontend/lib/components/preview/index.ts +2 -0
- package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
- package/frontend/lib/components/settings/SettingsView.svelte +36 -0
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
- package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
- package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
- package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
- package/frontend/lib/components/terminal/Terminal.svelte +368 -0
- package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
- package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
- package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
- package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
- package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
- package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
- package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
- package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
- package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
- package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
- package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
- package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
- package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
- package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
- package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
- package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
- package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
- package/frontend/lib/constants/preview.ts +45 -0
- package/frontend/lib/services/chat/chat.service.ts +704 -0
- package/frontend/lib/services/chat/index.ts +7 -0
- package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
- package/frontend/lib/services/notification/index.ts +8 -0
- package/frontend/lib/services/notification/push.service.ts +144 -0
- package/frontend/lib/services/notification/sound.service.ts +127 -0
- package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
- package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
- package/frontend/lib/services/preview/index.ts +23 -0
- package/frontend/lib/services/project/index.ts +8 -0
- package/frontend/lib/services/project/status.service.ts +159 -0
- package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
- package/frontend/lib/services/terminal/background/index.ts +130 -0
- package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
- package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
- package/frontend/lib/services/terminal/index.ts +14 -0
- package/frontend/lib/services/terminal/persistence.service.ts +260 -0
- package/frontend/lib/services/terminal/project.service.ts +953 -0
- package/frontend/lib/services/terminal/session.service.ts +364 -0
- package/frontend/lib/services/terminal/terminal.service.ts +369 -0
- package/frontend/lib/stores/core/app.svelte.ts +117 -0
- package/frontend/lib/stores/core/files.svelte.ts +73 -0
- package/frontend/lib/stores/core/presence.svelte.ts +48 -0
- package/frontend/lib/stores/core/projects.svelte.ts +317 -0
- package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
- package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
- package/frontend/lib/stores/features/models.svelte.ts +89 -0
- package/frontend/lib/stores/features/settings.svelte.ts +87 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
- package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
- package/frontend/lib/stores/features/user.svelte.ts +96 -0
- package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
- package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
- package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
- package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
- package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
- package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
- package/frontend/lib/types/native-ui.ts +73 -0
- package/frontend/lib/utils/chat/date-separator.ts +39 -0
- package/frontend/lib/utils/chat/message-grouper.ts +219 -0
- package/frontend/lib/utils/chat/message-processor.ts +135 -0
- package/frontend/lib/utils/chat/tool-handler.ts +161 -0
- package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
- package/frontend/lib/utils/click-outside.ts +20 -0
- package/frontend/lib/utils/context-manager.ts +257 -0
- package/frontend/lib/utils/file-icon-mappings.ts +769 -0
- package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
- package/frontend/lib/utils/git-status.ts +68 -0
- package/frontend/lib/utils/platform.ts +113 -0
- package/frontend/lib/utils/port-check.ts +65 -0
- package/frontend/lib/utils/terminalFormatter.ts +207 -0
- package/frontend/lib/utils/theme.ts +6 -0
- package/frontend/lib/utils/tree-visualizer.ts +320 -0
- package/frontend/lib/utils/ws.ts +44 -0
- package/frontend/main.ts +13 -0
- package/index.html +70 -0
- package/package.json +111 -0
- package/scripts/generate-icons.ts +87 -0
- package/scripts/pre-publish-check.sh +142 -0
- package/scripts/setup-hooks.sh +134 -0
- package/scripts/validate-branch-name.sh +47 -0
- package/scripts/validate-commit-msg.sh +42 -0
- package/shared/constants/engines.ts +134 -0
- package/shared/types/database/connection.ts +16 -0
- package/shared/types/database/index.ts +6 -0
- package/shared/types/database/schema.ts +141 -0
- package/shared/types/engine/index.ts +45 -0
- package/shared/types/filesystem/index.ts +22 -0
- package/shared/types/git.ts +171 -0
- package/shared/types/messaging/index.ts +239 -0
- package/shared/types/messaging/tool.ts +526 -0
- package/shared/types/network/api.ts +18 -0
- package/shared/types/network/index.ts +5 -0
- package/shared/types/stores/app.ts +23 -0
- package/shared/types/stores/dialog.ts +21 -0
- package/shared/types/stores/index.ts +3 -0
- package/shared/types/stores/settings.ts +15 -0
- package/shared/types/terminal/index.ts +44 -0
- package/shared/types/ui/components.ts +61 -0
- package/shared/types/ui/icons.ts +23 -0
- package/shared/types/ui/index.ts +22 -0
- package/shared/types/ui/notifications.ts +14 -0
- package/shared/types/ui/theme.ts +12 -0
- package/shared/types/websocket/index.ts +43 -0
- package/shared/types/window.d.ts +13 -0
- package/shared/utils/anonymous-user.ts +168 -0
- package/shared/utils/async.ts +10 -0
- package/shared/utils/diff-calculator.ts +184 -0
- package/shared/utils/file-type-detection.ts +166 -0
- package/shared/utils/logger.ts +158 -0
- package/shared/utils/message-formatter.ts +79 -0
- package/shared/utils/path.ts +47 -0
- package/shared/utils/ws-client.ts +768 -0
- package/shared/utils/ws-server.ts +660 -0
- package/static/audio/notification.ogg +0 -0
- package/static/favicon.svg +8 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
- package/static/fonts/dm-sans.css +96 -0
- package/svelte.config.js +20 -0
- 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@{{entry.index}}</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>
|