@atercates/claude-deck 0.2.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/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import {
|
|
6
|
+
GitBranch,
|
|
7
|
+
GitPullRequest,
|
|
8
|
+
RefreshCw,
|
|
9
|
+
Loader2,
|
|
10
|
+
AlertCircle,
|
|
11
|
+
ArrowUp,
|
|
12
|
+
ArrowDown,
|
|
13
|
+
Plus,
|
|
14
|
+
Minus,
|
|
15
|
+
ArrowLeft,
|
|
16
|
+
FileCode,
|
|
17
|
+
ExternalLink,
|
|
18
|
+
} from "lucide-react";
|
|
19
|
+
import { Button } from "@/components/ui/button";
|
|
20
|
+
import { FileChanges } from "./FileChanges";
|
|
21
|
+
import { CommitForm } from "./CommitForm";
|
|
22
|
+
import { PRCreationModal } from "@/components/PRCreationModal";
|
|
23
|
+
import { GitPanelTabs, type GitTab } from "./GitPanelTabs";
|
|
24
|
+
import { CommitHistory } from "./CommitHistory";
|
|
25
|
+
import { DiffView } from "@/components/DiffViewer/DiffModal";
|
|
26
|
+
import { useViewport } from "@/hooks/useViewport";
|
|
27
|
+
import {
|
|
28
|
+
useGitStatus,
|
|
29
|
+
usePRStatus,
|
|
30
|
+
useCreatePR,
|
|
31
|
+
useStageFiles,
|
|
32
|
+
useUnstageFiles,
|
|
33
|
+
useMultiRepoGitStatus,
|
|
34
|
+
gitKeys,
|
|
35
|
+
} from "@/data/git/queries";
|
|
36
|
+
import type { GitStatus, GitFile } from "@/lib/git-status";
|
|
37
|
+
import type { MultiRepoGitFile } from "@/lib/multi-repo-git";
|
|
38
|
+
import type { ProjectRepository } from "@/lib/db";
|
|
39
|
+
|
|
40
|
+
interface GitPanelProps {
|
|
41
|
+
workingDirectory: string;
|
|
42
|
+
projectId?: string;
|
|
43
|
+
repositories?: ProjectRepository[];
|
|
44
|
+
onFileSelect?: (file: GitFile, diff: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SelectedFile {
|
|
48
|
+
file: GitFile | MultiRepoGitFile;
|
|
49
|
+
diff: string;
|
|
50
|
+
repoPath?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function GitPanel({
|
|
54
|
+
workingDirectory,
|
|
55
|
+
projectId,
|
|
56
|
+
repositories = [],
|
|
57
|
+
}: GitPanelProps) {
|
|
58
|
+
const { isMobile } = useViewport();
|
|
59
|
+
const queryClient = useQueryClient();
|
|
60
|
+
const [activeTab, setActiveTab] = useState<GitTab>("changes");
|
|
61
|
+
const [showPRModal, setShowPRModal] = useState(false);
|
|
62
|
+
|
|
63
|
+
// Determine if we're in multi-repo mode
|
|
64
|
+
const isMultiRepo = repositories.length > 0;
|
|
65
|
+
|
|
66
|
+
// Single-repo mode hooks
|
|
67
|
+
const singleRepoQuery = useGitStatus(workingDirectory, {
|
|
68
|
+
enabled: !isMultiRepo,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Multi-repo mode hooks
|
|
72
|
+
const multiRepoQuery = useMultiRepoGitStatus(projectId, workingDirectory, {
|
|
73
|
+
enabled: isMultiRepo,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Unified status based on mode
|
|
77
|
+
const loading = isMultiRepo
|
|
78
|
+
? multiRepoQuery.isPending
|
|
79
|
+
: singleRepoQuery.isPending;
|
|
80
|
+
const isError = isMultiRepo
|
|
81
|
+
? multiRepoQuery.isError
|
|
82
|
+
: singleRepoQuery.isError;
|
|
83
|
+
const error = isMultiRepo ? multiRepoQuery.error : singleRepoQuery.error;
|
|
84
|
+
const isRefetching = isMultiRepo
|
|
85
|
+
? multiRepoQuery.isRefetching
|
|
86
|
+
: singleRepoQuery.isRefetching;
|
|
87
|
+
|
|
88
|
+
// Convert multi-repo status to single-repo-like status for unified handling
|
|
89
|
+
const status: GitStatus | null = isMultiRepo
|
|
90
|
+
? multiRepoQuery.data
|
|
91
|
+
? {
|
|
92
|
+
// Use first repo's branch or "Multiple"
|
|
93
|
+
branch:
|
|
94
|
+
multiRepoQuery.data.repositories.length === 1
|
|
95
|
+
? multiRepoQuery.data.repositories[0]?.branch || ""
|
|
96
|
+
: `${multiRepoQuery.data.repositories.length} repos`,
|
|
97
|
+
ahead: multiRepoQuery.data.repositories.reduce(
|
|
98
|
+
(sum, r) => sum + r.ahead,
|
|
99
|
+
0
|
|
100
|
+
),
|
|
101
|
+
behind: multiRepoQuery.data.repositories.reduce(
|
|
102
|
+
(sum, r) => sum + r.behind,
|
|
103
|
+
0
|
|
104
|
+
),
|
|
105
|
+
staged: multiRepoQuery.data.staged,
|
|
106
|
+
unstaged: multiRepoQuery.data.unstaged,
|
|
107
|
+
untracked: multiRepoQuery.data.untracked,
|
|
108
|
+
}
|
|
109
|
+
: null
|
|
110
|
+
: singleRepoQuery.data || null;
|
|
111
|
+
|
|
112
|
+
const refetchStatus = isMultiRepo
|
|
113
|
+
? multiRepoQuery.refetch
|
|
114
|
+
: singleRepoQuery.refetch;
|
|
115
|
+
|
|
116
|
+
// For PR status, use the primary repo or first repo in multi-repo mode
|
|
117
|
+
const primaryRepoPath = isMultiRepo
|
|
118
|
+
? repositories.find((r) => r.is_primary)?.path ||
|
|
119
|
+
repositories[0]?.path ||
|
|
120
|
+
workingDirectory
|
|
121
|
+
: workingDirectory;
|
|
122
|
+
|
|
123
|
+
const { data: prData } = usePRStatus(primaryRepoPath);
|
|
124
|
+
const existingPR = prData?.existingPR ?? null;
|
|
125
|
+
|
|
126
|
+
const createPRMutation = useCreatePR(primaryRepoPath);
|
|
127
|
+
const stageMutation = useStageFiles(primaryRepoPath);
|
|
128
|
+
const unstageMutation = useUnstageFiles(primaryRepoPath);
|
|
129
|
+
|
|
130
|
+
// Selected file for diff view
|
|
131
|
+
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
|
132
|
+
const [loadingDiff, setLoadingDiff] = useState(false);
|
|
133
|
+
|
|
134
|
+
// Resizable panel state (desktop)
|
|
135
|
+
const [listWidth, setListWidth] = useState(300);
|
|
136
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
137
|
+
const isDragging = useRef(false);
|
|
138
|
+
|
|
139
|
+
const handleRefresh = async () => {
|
|
140
|
+
await refetchStatus();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleFileClick = async (file: GitFile | MultiRepoGitFile) => {
|
|
144
|
+
setLoadingDiff(true);
|
|
145
|
+
try {
|
|
146
|
+
const isUntracked = file.status === "untracked";
|
|
147
|
+
// In multi-repo mode, use the file's repoPath
|
|
148
|
+
const repoPath =
|
|
149
|
+
"repoPath" in file && file.repoPath ? file.repoPath : workingDirectory;
|
|
150
|
+
const params = new URLSearchParams({
|
|
151
|
+
path: repoPath,
|
|
152
|
+
file: file.path,
|
|
153
|
+
staged: file.staged.toString(),
|
|
154
|
+
...(isUntracked && { untracked: "true" }),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const res = await fetch(`/api/git/status?${params}`);
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
|
|
160
|
+
if (data.diff !== undefined) {
|
|
161
|
+
setSelectedFile({ file, diff: data.diff, repoPath });
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Ignore errors
|
|
165
|
+
} finally {
|
|
166
|
+
setLoadingDiff(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleStage = async (file: GitFile | MultiRepoGitFile) => {
|
|
171
|
+
// In multi-repo mode, use the file's repoPath
|
|
172
|
+
const repoPath =
|
|
173
|
+
"repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
|
|
174
|
+
try {
|
|
175
|
+
await fetch("/api/git/stage", {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json" },
|
|
178
|
+
body: JSON.stringify({ path: repoPath, files: [file.path] }),
|
|
179
|
+
});
|
|
180
|
+
// Invalidate queries to refresh
|
|
181
|
+
queryClient.invalidateQueries({ queryKey: gitKeys.all });
|
|
182
|
+
// Update selected file's staged status if it's the same file
|
|
183
|
+
if (selectedFile?.file.path === file.path) {
|
|
184
|
+
setSelectedFile({ ...selectedFile, file: { ...file, staged: true } });
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Ignore errors
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleUnstage = async (file: GitFile | MultiRepoGitFile) => {
|
|
192
|
+
// In multi-repo mode, use the file's repoPath
|
|
193
|
+
const repoPath =
|
|
194
|
+
"repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
|
|
195
|
+
try {
|
|
196
|
+
await fetch("/api/git/unstage", {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: { "Content-Type": "application/json" },
|
|
199
|
+
body: JSON.stringify({ path: repoPath, files: [file.path] }),
|
|
200
|
+
});
|
|
201
|
+
// Invalidate queries to refresh
|
|
202
|
+
queryClient.invalidateQueries({ queryKey: gitKeys.all });
|
|
203
|
+
// Update selected file's staged status if it's the same file
|
|
204
|
+
if (selectedFile?.file.path === file.path) {
|
|
205
|
+
setSelectedFile({
|
|
206
|
+
...selectedFile,
|
|
207
|
+
file: { ...file, staged: false },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Ignore errors
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleStageAll = () => {
|
|
216
|
+
stageMutation.mutate(undefined);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handleUnstageAll = () => {
|
|
220
|
+
unstageMutation.mutate(undefined);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Resize handle for desktop
|
|
224
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
isDragging.current = true;
|
|
227
|
+
document.body.style.cursor = "col-resize";
|
|
228
|
+
document.body.style.userSelect = "none";
|
|
229
|
+
|
|
230
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
231
|
+
if (!isDragging.current || !containerRef.current) return;
|
|
232
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
233
|
+
const newWidth = e.clientX - containerRect.left;
|
|
234
|
+
setListWidth(Math.max(200, Math.min(500, newWidth)));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const handleMouseUp = () => {
|
|
238
|
+
isDragging.current = false;
|
|
239
|
+
document.body.style.cursor = "";
|
|
240
|
+
document.body.style.userSelect = "";
|
|
241
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
242
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
246
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
if (loading) {
|
|
250
|
+
return (
|
|
251
|
+
<div className="bg-background flex h-full w-full flex-col">
|
|
252
|
+
<Header
|
|
253
|
+
branch=""
|
|
254
|
+
ahead={0}
|
|
255
|
+
behind={0}
|
|
256
|
+
onRefresh={handleRefresh}
|
|
257
|
+
refreshing={false}
|
|
258
|
+
/>
|
|
259
|
+
<div className="flex flex-1 items-center justify-center">
|
|
260
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (isError) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="bg-background flex h-full w-full flex-col">
|
|
269
|
+
<Header
|
|
270
|
+
branch=""
|
|
271
|
+
ahead={0}
|
|
272
|
+
behind={0}
|
|
273
|
+
onRefresh={handleRefresh}
|
|
274
|
+
refreshing={isRefetching}
|
|
275
|
+
existingPR={existingPR}
|
|
276
|
+
/>
|
|
277
|
+
<div className="flex flex-1 flex-col items-center justify-center p-4">
|
|
278
|
+
<AlertCircle className="text-muted-foreground mb-2 h-8 w-8" />
|
|
279
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
280
|
+
{error?.message ?? "Failed to load git status"}
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!status) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const hasChanges =
|
|
292
|
+
status.staged.length > 0 ||
|
|
293
|
+
status.unstaged.length > 0 ||
|
|
294
|
+
status.untracked.length > 0;
|
|
295
|
+
|
|
296
|
+
// Mobile layout: full-screen list OR full-screen diff
|
|
297
|
+
if (isMobile) {
|
|
298
|
+
return (
|
|
299
|
+
<MobileGitPanel
|
|
300
|
+
status={status}
|
|
301
|
+
hasChanges={hasChanges}
|
|
302
|
+
selectedFile={selectedFile}
|
|
303
|
+
loadingDiff={loadingDiff}
|
|
304
|
+
refreshing={isRefetching}
|
|
305
|
+
showPRModal={showPRModal}
|
|
306
|
+
workingDirectory={workingDirectory}
|
|
307
|
+
activeTab={activeTab}
|
|
308
|
+
existingPR={existingPR}
|
|
309
|
+
creatingPR={createPRMutation.isPending}
|
|
310
|
+
onTabChange={setActiveTab}
|
|
311
|
+
onRefresh={handleRefresh}
|
|
312
|
+
onFileClick={handleFileClick}
|
|
313
|
+
onStage={handleStage}
|
|
314
|
+
onUnstage={handleUnstage}
|
|
315
|
+
onStageAll={handleStageAll}
|
|
316
|
+
onUnstageAll={handleUnstageAll}
|
|
317
|
+
onBack={() => setSelectedFile(null)}
|
|
318
|
+
onCommit={() => {
|
|
319
|
+
queryClient.invalidateQueries({
|
|
320
|
+
queryKey: gitKeys.status(workingDirectory),
|
|
321
|
+
});
|
|
322
|
+
queryClient.invalidateQueries({
|
|
323
|
+
queryKey: gitKeys.pr(workingDirectory),
|
|
324
|
+
});
|
|
325
|
+
}}
|
|
326
|
+
onShowPRModal={() => setShowPRModal(true)}
|
|
327
|
+
onClosePRModal={() => setShowPRModal(false)}
|
|
328
|
+
onCreatePR={() => createPRMutation.mutate()}
|
|
329
|
+
/>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Desktop layout: side-by-side for Changes, or CommitHistory for History
|
|
334
|
+
if (activeTab === "history") {
|
|
335
|
+
return (
|
|
336
|
+
<div className="bg-background flex h-full w-full flex-col">
|
|
337
|
+
<Header
|
|
338
|
+
branch={status.branch}
|
|
339
|
+
ahead={status.ahead}
|
|
340
|
+
behind={status.behind}
|
|
341
|
+
onRefresh={handleRefresh}
|
|
342
|
+
refreshing={isRefetching}
|
|
343
|
+
existingPR={existingPR}
|
|
344
|
+
/>
|
|
345
|
+
<GitPanelTabs activeTab={activeTab} onTabChange={setActiveTab} />
|
|
346
|
+
<CommitHistory workingDirectory={workingDirectory} />
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Desktop layout: side-by-side (Changes tab)
|
|
352
|
+
return (
|
|
353
|
+
<div
|
|
354
|
+
ref={containerRef}
|
|
355
|
+
className="bg-background flex h-full w-full flex-col"
|
|
356
|
+
>
|
|
357
|
+
<div className="flex min-h-0 flex-1">
|
|
358
|
+
{/* Left panel - file list */}
|
|
359
|
+
<div className="flex h-full flex-col" style={{ width: listWidth }}>
|
|
360
|
+
<Header
|
|
361
|
+
branch={status.branch}
|
|
362
|
+
ahead={status.ahead}
|
|
363
|
+
behind={status.behind}
|
|
364
|
+
onRefresh={handleRefresh}
|
|
365
|
+
refreshing={isRefetching}
|
|
366
|
+
/>
|
|
367
|
+
<GitPanelTabs activeTab={activeTab} onTabChange={setActiveTab} />
|
|
368
|
+
|
|
369
|
+
<div className="flex-1 overflow-y-auto">
|
|
370
|
+
{!hasChanges ? (
|
|
371
|
+
<div className="flex h-32 flex-col items-center justify-center gap-3">
|
|
372
|
+
<p className="text-muted-foreground text-sm">No changes</p>
|
|
373
|
+
{status.branch !== "main" &&
|
|
374
|
+
status.branch !== "master" &&
|
|
375
|
+
!existingPR && (
|
|
376
|
+
<Button
|
|
377
|
+
variant="outline"
|
|
378
|
+
size="sm"
|
|
379
|
+
onClick={() => createPRMutation.mutate()}
|
|
380
|
+
disabled={createPRMutation.isPending}
|
|
381
|
+
className="gap-1.5"
|
|
382
|
+
>
|
|
383
|
+
{createPRMutation.isPending ? (
|
|
384
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
385
|
+
) : (
|
|
386
|
+
<GitPullRequest className="h-3.5 w-3.5" />
|
|
387
|
+
)}
|
|
388
|
+
Create PR
|
|
389
|
+
</Button>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
) : (
|
|
393
|
+
<div className="py-2">
|
|
394
|
+
{/* Staged section */}
|
|
395
|
+
{status.staged.length > 0 && (
|
|
396
|
+
<FileChanges
|
|
397
|
+
files={status.staged}
|
|
398
|
+
title="Staged Changes"
|
|
399
|
+
emptyMessage="No staged changes"
|
|
400
|
+
selectedPath={selectedFile?.file.path}
|
|
401
|
+
onFileClick={handleFileClick}
|
|
402
|
+
onUnstage={handleUnstage}
|
|
403
|
+
onUnstageAll={handleUnstageAll}
|
|
404
|
+
isStaged={true}
|
|
405
|
+
/>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{/* Unstaged section */}
|
|
409
|
+
{status.unstaged.length > 0 && (
|
|
410
|
+
<FileChanges
|
|
411
|
+
files={status.unstaged}
|
|
412
|
+
title="Changes"
|
|
413
|
+
emptyMessage="No changes"
|
|
414
|
+
selectedPath={selectedFile?.file.path}
|
|
415
|
+
onFileClick={handleFileClick}
|
|
416
|
+
onStage={handleStage}
|
|
417
|
+
onStageAll={handleStageAll}
|
|
418
|
+
isStaged={false}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
{/* Untracked section */}
|
|
423
|
+
{status.untracked.length > 0 && (
|
|
424
|
+
<FileChanges
|
|
425
|
+
files={status.untracked}
|
|
426
|
+
title="Untracked Files"
|
|
427
|
+
emptyMessage="No untracked files"
|
|
428
|
+
selectedPath={selectedFile?.file.path}
|
|
429
|
+
onFileClick={handleFileClick}
|
|
430
|
+
onStage={handleStage}
|
|
431
|
+
isStaged={false}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{/* Commit form */}
|
|
439
|
+
<CommitForm
|
|
440
|
+
workingDirectory={workingDirectory}
|
|
441
|
+
stagedCount={status.staged.length}
|
|
442
|
+
branch={status.branch}
|
|
443
|
+
onCommit={() => {
|
|
444
|
+
queryClient.invalidateQueries({
|
|
445
|
+
queryKey: gitKeys.status(workingDirectory),
|
|
446
|
+
});
|
|
447
|
+
queryClient.invalidateQueries({
|
|
448
|
+
queryKey: gitKeys.pr(workingDirectory),
|
|
449
|
+
});
|
|
450
|
+
}}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{/* Resize handle */}
|
|
455
|
+
<div
|
|
456
|
+
className="bg-muted/50 hover:bg-primary/50 active:bg-primary w-1 flex-shrink-0 cursor-col-resize transition-colors"
|
|
457
|
+
onMouseDown={handleMouseDown}
|
|
458
|
+
/>
|
|
459
|
+
|
|
460
|
+
{/* Right panel - diff viewer */}
|
|
461
|
+
<div className="bg-muted/20 flex h-full min-w-0 flex-1 flex-col">
|
|
462
|
+
{loadingDiff ? (
|
|
463
|
+
<div className="flex flex-1 items-center justify-center">
|
|
464
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
465
|
+
</div>
|
|
466
|
+
) : selectedFile ? (
|
|
467
|
+
<>
|
|
468
|
+
{/* Diff header with stage/unstage */}
|
|
469
|
+
<div className="bg-background/50 flex items-center gap-2 p-3">
|
|
470
|
+
<FileCode className="text-muted-foreground h-4 w-4" />
|
|
471
|
+
<span className="flex-1 truncate text-sm font-medium">
|
|
472
|
+
{selectedFile.file.path}
|
|
473
|
+
</span>
|
|
474
|
+
<Button
|
|
475
|
+
variant={selectedFile.file.staged ? "outline" : "default"}
|
|
476
|
+
size="sm"
|
|
477
|
+
onClick={() =>
|
|
478
|
+
selectedFile.file.staged
|
|
479
|
+
? handleUnstage(selectedFile.file)
|
|
480
|
+
: handleStage(selectedFile.file)
|
|
481
|
+
}
|
|
482
|
+
>
|
|
483
|
+
{selectedFile.file.staged ? (
|
|
484
|
+
<>
|
|
485
|
+
<Minus className="mr-1 h-4 w-4" />
|
|
486
|
+
Unstage
|
|
487
|
+
</>
|
|
488
|
+
) : (
|
|
489
|
+
<>
|
|
490
|
+
<Plus className="mr-1 h-4 w-4" />
|
|
491
|
+
Stage
|
|
492
|
+
</>
|
|
493
|
+
)}
|
|
494
|
+
</Button>
|
|
495
|
+
</div>
|
|
496
|
+
{/* Diff content */}
|
|
497
|
+
<div className="flex-1 overflow-auto p-3">
|
|
498
|
+
<DiffView
|
|
499
|
+
diff={selectedFile.diff}
|
|
500
|
+
fileName={selectedFile.file.path}
|
|
501
|
+
/>
|
|
502
|
+
</div>
|
|
503
|
+
</>
|
|
504
|
+
) : (
|
|
505
|
+
<div className="text-muted-foreground flex flex-1 flex-col items-center justify-center">
|
|
506
|
+
<FileCode className="mb-4 h-12 w-12 opacity-50" />
|
|
507
|
+
<p className="text-sm">Select a file to view diff</p>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
{/* PR Creation Modal */}
|
|
514
|
+
{showPRModal && (
|
|
515
|
+
<PRCreationModal
|
|
516
|
+
workingDirectory={workingDirectory}
|
|
517
|
+
onClose={() => setShowPRModal(false)}
|
|
518
|
+
/>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Mobile layout component
|
|
525
|
+
interface MobileGitPanelProps {
|
|
526
|
+
status: GitStatus;
|
|
527
|
+
hasChanges: boolean;
|
|
528
|
+
selectedFile: SelectedFile | null;
|
|
529
|
+
loadingDiff: boolean;
|
|
530
|
+
refreshing: boolean;
|
|
531
|
+
showPRModal: boolean;
|
|
532
|
+
workingDirectory: string;
|
|
533
|
+
activeTab: GitTab;
|
|
534
|
+
existingPR: {
|
|
535
|
+
number: number;
|
|
536
|
+
url: string;
|
|
537
|
+
state: string;
|
|
538
|
+
title: string;
|
|
539
|
+
} | null;
|
|
540
|
+
creatingPR: boolean;
|
|
541
|
+
onTabChange: (tab: GitTab) => void;
|
|
542
|
+
onRefresh: () => void;
|
|
543
|
+
onFileClick: (file: GitFile) => void;
|
|
544
|
+
onStage: (file: GitFile) => void;
|
|
545
|
+
onUnstage: (file: GitFile) => void;
|
|
546
|
+
onStageAll: () => void;
|
|
547
|
+
onUnstageAll: () => void;
|
|
548
|
+
onBack: () => void;
|
|
549
|
+
onCommit: () => void;
|
|
550
|
+
onShowPRModal: () => void;
|
|
551
|
+
onClosePRModal: () => void;
|
|
552
|
+
onCreatePR: () => void;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function MobileGitPanel({
|
|
556
|
+
status,
|
|
557
|
+
hasChanges,
|
|
558
|
+
selectedFile,
|
|
559
|
+
loadingDiff,
|
|
560
|
+
refreshing,
|
|
561
|
+
showPRModal,
|
|
562
|
+
workingDirectory,
|
|
563
|
+
activeTab,
|
|
564
|
+
existingPR,
|
|
565
|
+
creatingPR,
|
|
566
|
+
onTabChange,
|
|
567
|
+
onRefresh,
|
|
568
|
+
onFileClick,
|
|
569
|
+
onStage,
|
|
570
|
+
onUnstage,
|
|
571
|
+
onStageAll,
|
|
572
|
+
onUnstageAll,
|
|
573
|
+
onBack,
|
|
574
|
+
onCommit,
|
|
575
|
+
onShowPRModal,
|
|
576
|
+
onClosePRModal,
|
|
577
|
+
onCreatePR,
|
|
578
|
+
}: MobileGitPanelProps) {
|
|
579
|
+
// History tab
|
|
580
|
+
if (activeTab === "history") {
|
|
581
|
+
return (
|
|
582
|
+
<div className="bg-background flex h-full w-full flex-col">
|
|
583
|
+
<Header
|
|
584
|
+
branch={status.branch}
|
|
585
|
+
ahead={status.ahead}
|
|
586
|
+
behind={status.behind}
|
|
587
|
+
onRefresh={onRefresh}
|
|
588
|
+
refreshing={refreshing}
|
|
589
|
+
existingPR={existingPR}
|
|
590
|
+
/>
|
|
591
|
+
<GitPanelTabs activeTab={activeTab} onTabChange={onTabChange} />
|
|
592
|
+
<CommitHistory workingDirectory={workingDirectory} />
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Show diff view when file is selected
|
|
598
|
+
if (selectedFile) {
|
|
599
|
+
return (
|
|
600
|
+
<div className="bg-background flex h-full w-full flex-col">
|
|
601
|
+
{/* Header */}
|
|
602
|
+
<div className="bg-muted/30 flex items-center gap-2 p-2">
|
|
603
|
+
<Button variant="ghost" size="icon-sm" onClick={onBack}>
|
|
604
|
+
<ArrowLeft className="h-5 w-5" />
|
|
605
|
+
</Button>
|
|
606
|
+
<div className="min-w-0 flex-1">
|
|
607
|
+
<p className="truncate text-sm font-medium">
|
|
608
|
+
{selectedFile.file.path}
|
|
609
|
+
</p>
|
|
610
|
+
</div>
|
|
611
|
+
<Button
|
|
612
|
+
variant={selectedFile.file.staged ? "outline" : "default"}
|
|
613
|
+
size="sm"
|
|
614
|
+
onClick={() =>
|
|
615
|
+
selectedFile.file.staged
|
|
616
|
+
? onUnstage(selectedFile.file)
|
|
617
|
+
: onStage(selectedFile.file)
|
|
618
|
+
}
|
|
619
|
+
>
|
|
620
|
+
{selectedFile.file.staged ? "Unstage" : "Stage"}
|
|
621
|
+
</Button>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
{/* Diff content */}
|
|
625
|
+
<div className="flex-1 overflow-auto p-3">
|
|
626
|
+
{loadingDiff ? (
|
|
627
|
+
<div className="flex h-32 items-center justify-center">
|
|
628
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
629
|
+
</div>
|
|
630
|
+
) : (
|
|
631
|
+
<DiffView
|
|
632
|
+
diff={selectedFile.diff}
|
|
633
|
+
fileName={selectedFile.file.path}
|
|
634
|
+
/>
|
|
635
|
+
)}
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Show file list (Changes tab)
|
|
642
|
+
return (
|
|
643
|
+
<div className="bg-background flex h-full w-full flex-col">
|
|
644
|
+
<Header
|
|
645
|
+
branch={status.branch}
|
|
646
|
+
ahead={status.ahead}
|
|
647
|
+
behind={status.behind}
|
|
648
|
+
onRefresh={onRefresh}
|
|
649
|
+
refreshing={refreshing}
|
|
650
|
+
existingPR={existingPR}
|
|
651
|
+
/>
|
|
652
|
+
<GitPanelTabs activeTab={activeTab} onTabChange={onTabChange} />
|
|
653
|
+
|
|
654
|
+
<div className="flex-1 overflow-y-auto">
|
|
655
|
+
{!hasChanges ? (
|
|
656
|
+
<div className="flex h-32 flex-col items-center justify-center gap-3">
|
|
657
|
+
<p className="text-muted-foreground text-sm">No changes</p>
|
|
658
|
+
{status.branch !== "main" &&
|
|
659
|
+
status.branch !== "master" &&
|
|
660
|
+
!existingPR && (
|
|
661
|
+
<Button
|
|
662
|
+
variant="outline"
|
|
663
|
+
size="sm"
|
|
664
|
+
onClick={onCreatePR}
|
|
665
|
+
disabled={creatingPR}
|
|
666
|
+
className="gap-1.5"
|
|
667
|
+
>
|
|
668
|
+
{creatingPR ? (
|
|
669
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
670
|
+
) : (
|
|
671
|
+
<GitPullRequest className="h-3.5 w-3.5" />
|
|
672
|
+
)}
|
|
673
|
+
Create PR
|
|
674
|
+
</Button>
|
|
675
|
+
)}
|
|
676
|
+
</div>
|
|
677
|
+
) : (
|
|
678
|
+
<div className="py-2">
|
|
679
|
+
{/* Staged section */}
|
|
680
|
+
{status.staged.length > 0 && (
|
|
681
|
+
<FileChanges
|
|
682
|
+
files={status.staged}
|
|
683
|
+
title="Staged Changes"
|
|
684
|
+
emptyMessage="No staged changes"
|
|
685
|
+
onFileClick={onFileClick}
|
|
686
|
+
onUnstage={onUnstage}
|
|
687
|
+
onUnstageAll={onUnstageAll}
|
|
688
|
+
isStaged={true}
|
|
689
|
+
/>
|
|
690
|
+
)}
|
|
691
|
+
|
|
692
|
+
{/* Unstaged section */}
|
|
693
|
+
{status.unstaged.length > 0 && (
|
|
694
|
+
<FileChanges
|
|
695
|
+
files={status.unstaged}
|
|
696
|
+
title="Changes"
|
|
697
|
+
emptyMessage="No changes"
|
|
698
|
+
onFileClick={onFileClick}
|
|
699
|
+
onStage={onStage}
|
|
700
|
+
onStageAll={onStageAll}
|
|
701
|
+
isStaged={false}
|
|
702
|
+
/>
|
|
703
|
+
)}
|
|
704
|
+
|
|
705
|
+
{/* Untracked section */}
|
|
706
|
+
{status.untracked.length > 0 && (
|
|
707
|
+
<FileChanges
|
|
708
|
+
files={status.untracked}
|
|
709
|
+
title="Untracked Files"
|
|
710
|
+
emptyMessage="No untracked files"
|
|
711
|
+
onFileClick={onFileClick}
|
|
712
|
+
onStage={onStage}
|
|
713
|
+
isStaged={false}
|
|
714
|
+
/>
|
|
715
|
+
)}
|
|
716
|
+
</div>
|
|
717
|
+
)}
|
|
718
|
+
</div>
|
|
719
|
+
|
|
720
|
+
{/* Commit form */}
|
|
721
|
+
<CommitForm
|
|
722
|
+
workingDirectory={workingDirectory}
|
|
723
|
+
stagedCount={status.staged.length}
|
|
724
|
+
branch={status.branch}
|
|
725
|
+
onCommit={onCommit}
|
|
726
|
+
/>
|
|
727
|
+
|
|
728
|
+
{/* Mobile hint */}
|
|
729
|
+
{hasChanges && status.staged.length === 0 && (
|
|
730
|
+
<div className="px-3 py-2">
|
|
731
|
+
<p className="text-muted-foreground text-center text-xs">
|
|
732
|
+
Swipe right to stage, left to unstage
|
|
733
|
+
</p>
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
|
|
737
|
+
{/* PR Creation Modal */}
|
|
738
|
+
{showPRModal && (
|
|
739
|
+
<PRCreationModal
|
|
740
|
+
workingDirectory={workingDirectory}
|
|
741
|
+
onClose={onClosePRModal}
|
|
742
|
+
/>
|
|
743
|
+
)}
|
|
744
|
+
</div>
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
interface HeaderProps {
|
|
749
|
+
branch: string;
|
|
750
|
+
ahead: number;
|
|
751
|
+
behind: number;
|
|
752
|
+
onRefresh: () => void;
|
|
753
|
+
refreshing: boolean;
|
|
754
|
+
existingPR?: {
|
|
755
|
+
number: number;
|
|
756
|
+
url: string;
|
|
757
|
+
title: string;
|
|
758
|
+
} | null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function Header({
|
|
762
|
+
branch,
|
|
763
|
+
ahead,
|
|
764
|
+
behind,
|
|
765
|
+
onRefresh,
|
|
766
|
+
refreshing,
|
|
767
|
+
existingPR,
|
|
768
|
+
}: HeaderProps) {
|
|
769
|
+
return (
|
|
770
|
+
<div className="flex items-center gap-2 p-3">
|
|
771
|
+
<GitBranch className="text-muted-foreground h-4 w-4 flex-shrink-0" />
|
|
772
|
+
<div className="min-w-0 flex-1">
|
|
773
|
+
<div className="flex items-center gap-2">
|
|
774
|
+
<p className="truncate text-sm font-medium">
|
|
775
|
+
{branch || "Git Status"}
|
|
776
|
+
</p>
|
|
777
|
+
{existingPR && (
|
|
778
|
+
<button
|
|
779
|
+
onClick={() => window.open(existingPR.url, "_blank")}
|
|
780
|
+
className="bg-muted hover:bg-accent inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs transition-colors"
|
|
781
|
+
title={`${existingPR.title} (#${existingPR.number})`}
|
|
782
|
+
>
|
|
783
|
+
<GitPullRequest className="h-3 w-3" />
|
|
784
|
+
PR
|
|
785
|
+
<ExternalLink className="h-2.5 w-2.5" />
|
|
786
|
+
</button>
|
|
787
|
+
)}
|
|
788
|
+
</div>
|
|
789
|
+
{(ahead > 0 || behind > 0) && (
|
|
790
|
+
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
|
791
|
+
{ahead > 0 && (
|
|
792
|
+
<span className="flex items-center gap-0.5">
|
|
793
|
+
<ArrowUp className="h-3 w-3" />
|
|
794
|
+
{ahead}
|
|
795
|
+
</span>
|
|
796
|
+
)}
|
|
797
|
+
{behind > 0 && (
|
|
798
|
+
<span className="flex items-center gap-0.5">
|
|
799
|
+
<ArrowDown className="h-3 w-3" />
|
|
800
|
+
{behind}
|
|
801
|
+
</span>
|
|
802
|
+
)}
|
|
803
|
+
</div>
|
|
804
|
+
)}
|
|
805
|
+
</div>
|
|
806
|
+
<Button
|
|
807
|
+
variant="ghost"
|
|
808
|
+
size="icon-sm"
|
|
809
|
+
onClick={onRefresh}
|
|
810
|
+
disabled={refreshing}
|
|
811
|
+
className="h-8 w-8"
|
|
812
|
+
>
|
|
813
|
+
<RefreshCw className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`} />
|
|
814
|
+
</Button>
|
|
815
|
+
</div>
|
|
816
|
+
);
|
|
817
|
+
}
|