@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,464 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import {
|
|
6
|
+
GitBranch,
|
|
7
|
+
RefreshCw,
|
|
8
|
+
Loader2,
|
|
9
|
+
AlertCircle,
|
|
10
|
+
ArrowUp,
|
|
11
|
+
ArrowDown,
|
|
12
|
+
X,
|
|
13
|
+
AlertTriangle,
|
|
14
|
+
ExternalLink,
|
|
15
|
+
GitPullRequest,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import {
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogContent,
|
|
21
|
+
DialogDescription,
|
|
22
|
+
DialogFooter,
|
|
23
|
+
DialogHeader,
|
|
24
|
+
DialogTitle,
|
|
25
|
+
} from "@/components/ui/dialog";
|
|
26
|
+
import { FileChanges } from "@/components/GitPanel/FileChanges";
|
|
27
|
+
import { CommitForm } from "@/components/GitPanel/CommitForm";
|
|
28
|
+
import { FileEditDialog } from "./FileEditDialog";
|
|
29
|
+
import { cn } from "@/lib/utils";
|
|
30
|
+
import { useDrawerAnimation } from "@/hooks/useDrawerAnimation";
|
|
31
|
+
import {
|
|
32
|
+
useGitStatus,
|
|
33
|
+
usePRStatus,
|
|
34
|
+
useCreatePR,
|
|
35
|
+
useStageFiles,
|
|
36
|
+
useUnstageFiles,
|
|
37
|
+
useMultiRepoGitStatus,
|
|
38
|
+
gitKeys,
|
|
39
|
+
} from "@/data/git/queries";
|
|
40
|
+
import type { GitFile } from "@/lib/git-status";
|
|
41
|
+
import type { MultiRepoGitFile } from "@/lib/multi-repo-git";
|
|
42
|
+
import type { ProjectRepository } from "@/lib/db";
|
|
43
|
+
|
|
44
|
+
interface GitDrawerProps {
|
|
45
|
+
open: boolean;
|
|
46
|
+
onOpenChange: (open: boolean) => void;
|
|
47
|
+
workingDirectory: string;
|
|
48
|
+
projectId?: string;
|
|
49
|
+
repositories?: ProjectRepository[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function GitDrawer({
|
|
53
|
+
open,
|
|
54
|
+
onOpenChange,
|
|
55
|
+
workingDirectory,
|
|
56
|
+
projectId,
|
|
57
|
+
repositories = [],
|
|
58
|
+
}: GitDrawerProps) {
|
|
59
|
+
const queryClient = useQueryClient();
|
|
60
|
+
|
|
61
|
+
// Determine if we're in multi-repo mode
|
|
62
|
+
const isMultiRepo = repositories.length > 0;
|
|
63
|
+
|
|
64
|
+
// Single-repo mode hooks - only poll when drawer is open
|
|
65
|
+
const singleRepoQuery = useGitStatus(workingDirectory, {
|
|
66
|
+
enabled: open && !isMultiRepo,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Multi-repo mode hooks
|
|
70
|
+
const multiRepoQuery = useMultiRepoGitStatus(projectId, workingDirectory, {
|
|
71
|
+
enabled: open && isMultiRepo,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Unified status based on mode
|
|
75
|
+
const loading = isMultiRepo
|
|
76
|
+
? multiRepoQuery.isPending
|
|
77
|
+
: singleRepoQuery.isPending;
|
|
78
|
+
const isError = isMultiRepo
|
|
79
|
+
? multiRepoQuery.isError
|
|
80
|
+
: singleRepoQuery.isError;
|
|
81
|
+
const error = isMultiRepo ? multiRepoQuery.error : singleRepoQuery.error;
|
|
82
|
+
const isRefetching = isMultiRepo
|
|
83
|
+
? multiRepoQuery.isRefetching
|
|
84
|
+
: singleRepoQuery.isRefetching;
|
|
85
|
+
|
|
86
|
+
// Convert to unified status
|
|
87
|
+
const status = isMultiRepo
|
|
88
|
+
? multiRepoQuery.data
|
|
89
|
+
? {
|
|
90
|
+
branch:
|
|
91
|
+
multiRepoQuery.data.repositories.length === 1
|
|
92
|
+
? multiRepoQuery.data.repositories[0]?.branch || ""
|
|
93
|
+
: `${multiRepoQuery.data.repositories.length} repos`,
|
|
94
|
+
ahead: multiRepoQuery.data.repositories.reduce(
|
|
95
|
+
(sum, r) => sum + r.ahead,
|
|
96
|
+
0
|
|
97
|
+
),
|
|
98
|
+
behind: multiRepoQuery.data.repositories.reduce(
|
|
99
|
+
(sum, r) => sum + r.behind,
|
|
100
|
+
0
|
|
101
|
+
),
|
|
102
|
+
staged: multiRepoQuery.data.staged,
|
|
103
|
+
unstaged: multiRepoQuery.data.unstaged,
|
|
104
|
+
untracked: multiRepoQuery.data.untracked,
|
|
105
|
+
}
|
|
106
|
+
: null
|
|
107
|
+
: singleRepoQuery.data || null;
|
|
108
|
+
|
|
109
|
+
const refetchStatus = isMultiRepo
|
|
110
|
+
? multiRepoQuery.refetch
|
|
111
|
+
: singleRepoQuery.refetch;
|
|
112
|
+
|
|
113
|
+
// For PR status, use the primary repo or first repo in multi-repo mode
|
|
114
|
+
const primaryRepoPath = isMultiRepo
|
|
115
|
+
? repositories.find((r) => r.is_primary)?.path ||
|
|
116
|
+
repositories[0]?.path ||
|
|
117
|
+
workingDirectory
|
|
118
|
+
: workingDirectory;
|
|
119
|
+
|
|
120
|
+
const { data: prData } = usePRStatus(primaryRepoPath);
|
|
121
|
+
const existingPR = prData?.existingPR ?? null;
|
|
122
|
+
|
|
123
|
+
const createPRMutation = useCreatePR(primaryRepoPath);
|
|
124
|
+
const stageMutation = useStageFiles(primaryRepoPath);
|
|
125
|
+
const unstageMutation = useUnstageFiles(primaryRepoPath);
|
|
126
|
+
|
|
127
|
+
// Local UI state
|
|
128
|
+
const [selectedFile, setSelectedFile] = useState<
|
|
129
|
+
GitFile | MultiRepoGitFile | null
|
|
130
|
+
>(null);
|
|
131
|
+
const [discardFile, setDiscardFile] = useState<
|
|
132
|
+
GitFile | MultiRepoGitFile | null
|
|
133
|
+
>(null);
|
|
134
|
+
const [discarding, setDiscarding] = useState(false);
|
|
135
|
+
|
|
136
|
+
// Animation
|
|
137
|
+
const isAnimatingIn = useDrawerAnimation(open);
|
|
138
|
+
|
|
139
|
+
// Clear selected file when drawer opens
|
|
140
|
+
const handleFileClick = (file: GitFile | MultiRepoGitFile) => {
|
|
141
|
+
setSelectedFile(file);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleStage = async (file: GitFile | MultiRepoGitFile) => {
|
|
145
|
+
// In multi-repo mode, use the file's repoPath
|
|
146
|
+
const repoPath =
|
|
147
|
+
"repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
|
|
148
|
+
try {
|
|
149
|
+
await fetch("/api/git/stage", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify({ path: repoPath, files: [file.path] }),
|
|
153
|
+
});
|
|
154
|
+
queryClient.invalidateQueries({ queryKey: gitKeys.all });
|
|
155
|
+
} catch {
|
|
156
|
+
// Ignore errors
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleUnstage = async (file: GitFile | MultiRepoGitFile) => {
|
|
161
|
+
// In multi-repo mode, use the file's repoPath
|
|
162
|
+
const repoPath =
|
|
163
|
+
"repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
|
|
164
|
+
try {
|
|
165
|
+
await fetch("/api/git/unstage", {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
body: JSON.stringify({ path: repoPath, files: [file.path] }),
|
|
169
|
+
});
|
|
170
|
+
queryClient.invalidateQueries({ queryKey: gitKeys.all });
|
|
171
|
+
} catch {
|
|
172
|
+
// Ignore errors
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleStageAll = () => {
|
|
177
|
+
stageMutation.mutate(undefined);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleUnstageAll = () => {
|
|
181
|
+
unstageMutation.mutate(undefined);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleDiscardConfirm = async () => {
|
|
185
|
+
if (!discardFile) return;
|
|
186
|
+
|
|
187
|
+
setDiscarding(true);
|
|
188
|
+
try {
|
|
189
|
+
// In multi-repo mode, use the file's repoPath
|
|
190
|
+
const repoPath =
|
|
191
|
+
"repoPath" in discardFile && discardFile.repoPath
|
|
192
|
+
? discardFile.repoPath
|
|
193
|
+
: primaryRepoPath;
|
|
194
|
+
await fetch("/api/git/discard", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
path: repoPath,
|
|
199
|
+
file: discardFile.path,
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
queryClient.invalidateQueries({ queryKey: gitKeys.all });
|
|
203
|
+
setDiscardFile(null);
|
|
204
|
+
} catch {
|
|
205
|
+
// Ignore errors
|
|
206
|
+
} finally {
|
|
207
|
+
setDiscarding(false);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const stagedFiles = status?.staged || [];
|
|
212
|
+
const unstagedFiles = [
|
|
213
|
+
...(status?.unstaged || []),
|
|
214
|
+
...(status?.untracked || []),
|
|
215
|
+
];
|
|
216
|
+
const isOnMainBranch = ["main", "master"].includes(status?.branch || "");
|
|
217
|
+
|
|
218
|
+
// In multi-repo mode, determine which repo has staged changes for commit
|
|
219
|
+
const reposWithStagedChanges =
|
|
220
|
+
isMultiRepo && multiRepoQuery.data
|
|
221
|
+
? multiRepoQuery.data.repositories.filter((repo) =>
|
|
222
|
+
multiRepoQuery.data!.staged.some((f) => f.repoId === repo.id)
|
|
223
|
+
)
|
|
224
|
+
: [];
|
|
225
|
+
|
|
226
|
+
// Use the first repo with staged changes, or fall back to primary repo
|
|
227
|
+
const commitRepoPath =
|
|
228
|
+
reposWithStagedChanges.length > 0
|
|
229
|
+
? reposWithStagedChanges[0].path
|
|
230
|
+
: primaryRepoPath;
|
|
231
|
+
|
|
232
|
+
const commitRepoName =
|
|
233
|
+
reposWithStagedChanges.length > 0
|
|
234
|
+
? reposWithStagedChanges[0].name
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
const commitRepoBranch =
|
|
238
|
+
reposWithStagedChanges.length > 0
|
|
239
|
+
? reposWithStagedChanges[0].branch
|
|
240
|
+
: status?.branch || "";
|
|
241
|
+
|
|
242
|
+
const multipleReposHaveStagedChanges = reposWithStagedChanges.length > 1;
|
|
243
|
+
|
|
244
|
+
if (!open) return null;
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<>
|
|
248
|
+
<div
|
|
249
|
+
className={cn(
|
|
250
|
+
"bg-muted/30 flex h-full flex-col transition-all duration-200 ease-out",
|
|
251
|
+
isAnimatingIn
|
|
252
|
+
? "translate-x-0 opacity-100"
|
|
253
|
+
: "translate-x-4 opacity-0"
|
|
254
|
+
)}
|
|
255
|
+
>
|
|
256
|
+
{/* Header */}
|
|
257
|
+
<div className="px-3 py-2">
|
|
258
|
+
<div className="flex items-center justify-between">
|
|
259
|
+
<div className="flex items-center gap-2">
|
|
260
|
+
<span className="text-sm font-medium">Git Changes</span>
|
|
261
|
+
{status && (
|
|
262
|
+
<span className="bg-muted rounded-full px-2 py-0.5 text-xs">
|
|
263
|
+
<GitBranch className="mr-1 inline h-3 w-3" />
|
|
264
|
+
{status.branch}
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
267
|
+
{existingPR && (
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => window.open(existingPR.url, "_blank")}
|
|
270
|
+
className="bg-muted hover:bg-accent inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs transition-colors"
|
|
271
|
+
title={`${existingPR.title} (#${existingPR.number})`}
|
|
272
|
+
>
|
|
273
|
+
<GitPullRequest className="h-3 w-3" />
|
|
274
|
+
View PR
|
|
275
|
+
<ExternalLink className="h-2.5 w-2.5" />
|
|
276
|
+
</button>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
<div className="flex items-center gap-1">
|
|
280
|
+
<Button
|
|
281
|
+
variant="ghost"
|
|
282
|
+
size="icon"
|
|
283
|
+
onClick={() => refetchStatus()}
|
|
284
|
+
disabled={isRefetching || loading}
|
|
285
|
+
className="h-7 w-7"
|
|
286
|
+
>
|
|
287
|
+
<RefreshCw
|
|
288
|
+
className={cn("h-3.5 w-3.5", isRefetching && "animate-spin")}
|
|
289
|
+
/>
|
|
290
|
+
</Button>
|
|
291
|
+
<Button
|
|
292
|
+
variant="ghost"
|
|
293
|
+
size="icon"
|
|
294
|
+
onClick={() => onOpenChange(false)}
|
|
295
|
+
className="h-7 w-7"
|
|
296
|
+
>
|
|
297
|
+
<X className="h-3.5 w-3.5" />
|
|
298
|
+
</Button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{/* Ahead/behind indicator */}
|
|
303
|
+
{status && (status.ahead > 0 || status.behind > 0) && (
|
|
304
|
+
<div className="text-muted-foreground mt-1 flex items-center gap-3 text-xs">
|
|
305
|
+
{status.ahead > 0 && (
|
|
306
|
+
<span className="flex items-center gap-1">
|
|
307
|
+
<ArrowUp className="h-3 w-3" />
|
|
308
|
+
{status.ahead} ahead
|
|
309
|
+
</span>
|
|
310
|
+
)}
|
|
311
|
+
{status.behind > 0 && (
|
|
312
|
+
<span className="flex items-center gap-1">
|
|
313
|
+
<ArrowDown className="h-3 w-3" />
|
|
314
|
+
{status.behind} behind
|
|
315
|
+
</span>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Content */}
|
|
322
|
+
<div className="flex-1 overflow-y-auto py-2">
|
|
323
|
+
{loading ? (
|
|
324
|
+
<div className="flex items-center justify-center py-8">
|
|
325
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
326
|
+
</div>
|
|
327
|
+
) : isError ? (
|
|
328
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
329
|
+
<AlertCircle className="h-8 w-8 text-red-500" />
|
|
330
|
+
<p className="text-muted-foreground text-sm">
|
|
331
|
+
{error?.message ?? "Failed to load git status"}
|
|
332
|
+
</p>
|
|
333
|
+
<Button
|
|
334
|
+
variant="outline"
|
|
335
|
+
size="sm"
|
|
336
|
+
onClick={() => refetchStatus()}
|
|
337
|
+
>
|
|
338
|
+
Retry
|
|
339
|
+
</Button>
|
|
340
|
+
</div>
|
|
341
|
+
) : stagedFiles.length === 0 && unstagedFiles.length === 0 ? (
|
|
342
|
+
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
|
343
|
+
<span className="text-muted-foreground text-sm">No changes</span>
|
|
344
|
+
{!isOnMainBranch && !existingPR && (
|
|
345
|
+
<Button
|
|
346
|
+
variant="outline"
|
|
347
|
+
size="sm"
|
|
348
|
+
onClick={() => createPRMutation.mutate()}
|
|
349
|
+
disabled={createPRMutation.isPending}
|
|
350
|
+
className="gap-1.5"
|
|
351
|
+
>
|
|
352
|
+
{createPRMutation.isPending ? (
|
|
353
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
354
|
+
) : (
|
|
355
|
+
<GitPullRequest className="h-3.5 w-3.5" />
|
|
356
|
+
)}
|
|
357
|
+
Create PR
|
|
358
|
+
</Button>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
) : (
|
|
362
|
+
<>
|
|
363
|
+
{/* Staged files */}
|
|
364
|
+
<FileChanges
|
|
365
|
+
files={stagedFiles}
|
|
366
|
+
title="Staged Changes"
|
|
367
|
+
emptyMessage="No staged changes"
|
|
368
|
+
onFileClick={handleFileClick}
|
|
369
|
+
onUnstage={handleUnstage}
|
|
370
|
+
onUnstageAll={handleUnstageAll}
|
|
371
|
+
isStaged={true}
|
|
372
|
+
groupByRepo={isMultiRepo}
|
|
373
|
+
/>
|
|
374
|
+
|
|
375
|
+
{/* Unstaged files */}
|
|
376
|
+
<FileChanges
|
|
377
|
+
files={unstagedFiles}
|
|
378
|
+
title="Unstaged Changes"
|
|
379
|
+
emptyMessage="No unstaged changes"
|
|
380
|
+
onFileClick={handleFileClick}
|
|
381
|
+
onStage={handleStage}
|
|
382
|
+
onStageAll={handleStageAll}
|
|
383
|
+
onDiscard={setDiscardFile}
|
|
384
|
+
isStaged={false}
|
|
385
|
+
groupByRepo={isMultiRepo}
|
|
386
|
+
/>
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{/* Commit form at bottom */}
|
|
392
|
+
{status && (
|
|
393
|
+
<CommitForm
|
|
394
|
+
workingDirectory={commitRepoPath}
|
|
395
|
+
stagedCount={stagedFiles.length}
|
|
396
|
+
branch={commitRepoBranch}
|
|
397
|
+
repoName={isMultiRepo ? commitRepoName : undefined}
|
|
398
|
+
multipleReposWarning={multipleReposHaveStagedChanges}
|
|
399
|
+
onCommit={() => {
|
|
400
|
+
queryClient.invalidateQueries({ queryKey: gitKeys.all });
|
|
401
|
+
}}
|
|
402
|
+
/>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{/* File Edit Dialog */}
|
|
407
|
+
{selectedFile && (
|
|
408
|
+
<FileEditDialog
|
|
409
|
+
open={!!selectedFile}
|
|
410
|
+
onOpenChange={(open) => !open && setSelectedFile(null)}
|
|
411
|
+
workingDirectory={workingDirectory}
|
|
412
|
+
file={selectedFile}
|
|
413
|
+
allFiles={[...stagedFiles, ...unstagedFiles]}
|
|
414
|
+
onFileSelect={setSelectedFile}
|
|
415
|
+
onStage={handleStage}
|
|
416
|
+
onUnstage={handleUnstage}
|
|
417
|
+
onSave={() =>
|
|
418
|
+
queryClient.invalidateQueries({
|
|
419
|
+
queryKey: gitKeys.status(workingDirectory),
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
/>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{/* Discard Confirmation Modal */}
|
|
426
|
+
<Dialog
|
|
427
|
+
open={!!discardFile}
|
|
428
|
+
onOpenChange={(o) => !o && setDiscardFile(null)}
|
|
429
|
+
>
|
|
430
|
+
<DialogContent>
|
|
431
|
+
<DialogHeader>
|
|
432
|
+
<DialogTitle className="flex items-center gap-2">
|
|
433
|
+
<AlertTriangle className="h-5 w-5 text-red-500" />
|
|
434
|
+
Discard Changes
|
|
435
|
+
</DialogTitle>
|
|
436
|
+
<DialogDescription>
|
|
437
|
+
Are you sure you want to discard changes to{" "}
|
|
438
|
+
<span className="font-mono font-medium">
|
|
439
|
+
{discardFile?.path.split("/").pop()}
|
|
440
|
+
</span>
|
|
441
|
+
? This action cannot be undone.
|
|
442
|
+
</DialogDescription>
|
|
443
|
+
</DialogHeader>
|
|
444
|
+
<DialogFooter>
|
|
445
|
+
<Button
|
|
446
|
+
variant="outline"
|
|
447
|
+
onClick={() => setDiscardFile(null)}
|
|
448
|
+
disabled={discarding}
|
|
449
|
+
>
|
|
450
|
+
Cancel
|
|
451
|
+
</Button>
|
|
452
|
+
<Button
|
|
453
|
+
variant="destructive"
|
|
454
|
+
onClick={handleDiscardConfirm}
|
|
455
|
+
disabled={discarding}
|
|
456
|
+
>
|
|
457
|
+
{discarding ? "Discarding..." : "Discard"}
|
|
458
|
+
</Button>
|
|
459
|
+
</DialogFooter>
|
|
460
|
+
</DialogContent>
|
|
461
|
+
</Dialog>
|
|
462
|
+
</>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
GitCommit,
|
|
6
|
+
GitBranch,
|
|
7
|
+
Send,
|
|
8
|
+
Loader2,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
|
|
14
|
+
interface CommitFormProps {
|
|
15
|
+
workingDirectory: string;
|
|
16
|
+
stagedCount: number;
|
|
17
|
+
branch: string;
|
|
18
|
+
repoName?: string;
|
|
19
|
+
multipleReposWarning?: boolean;
|
|
20
|
+
onCommit: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CommitForm({
|
|
24
|
+
workingDirectory,
|
|
25
|
+
stagedCount,
|
|
26
|
+
branch,
|
|
27
|
+
repoName,
|
|
28
|
+
multipleReposWarning,
|
|
29
|
+
onCommit,
|
|
30
|
+
}: CommitFormProps) {
|
|
31
|
+
const [message, setMessage] = useState("");
|
|
32
|
+
const [committing, setCommitting] = useState(false);
|
|
33
|
+
const [pushing, setPushing] = useState(false);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const canCommit = stagedCount > 0 && message.trim().length > 0;
|
|
38
|
+
|
|
39
|
+
const handleCommit = async (): Promise<boolean> => {
|
|
40
|
+
if (!canCommit) return false;
|
|
41
|
+
|
|
42
|
+
setError(null);
|
|
43
|
+
setSuccess(null);
|
|
44
|
+
setCommitting(true);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch("/api/git/commit", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
path: workingDirectory,
|
|
52
|
+
message: message.trim(),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
|
|
58
|
+
if (!res.ok || data.error) {
|
|
59
|
+
setError(data.error || "Commit failed");
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Clear form
|
|
64
|
+
setMessage("");
|
|
65
|
+
setSuccess("Committed successfully!");
|
|
66
|
+
onCommit();
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
setError("Failed to commit");
|
|
70
|
+
return false;
|
|
71
|
+
} finally {
|
|
72
|
+
setCommitting(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handlePush = async () => {
|
|
77
|
+
setError(null);
|
|
78
|
+
setSuccess(null);
|
|
79
|
+
setPushing(true);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch("/api/git/push", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ path: workingDirectory }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
|
|
90
|
+
if (data.error) {
|
|
91
|
+
setError(data.error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (data.pushed) {
|
|
96
|
+
setSuccess("Pushed successfully!");
|
|
97
|
+
} else {
|
|
98
|
+
setSuccess(data.message || "Already up to date");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onCommit();
|
|
102
|
+
} catch {
|
|
103
|
+
setError("Failed to push");
|
|
104
|
+
} finally {
|
|
105
|
+
setPushing(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleCommitAndPush = async () => {
|
|
110
|
+
const commitSucceeded = await handleCommit();
|
|
111
|
+
// Only push if commit was successful
|
|
112
|
+
if (commitSucceeded) {
|
|
113
|
+
await handlePush();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Only show commit form when there are staged files
|
|
118
|
+
if (stagedCount === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="bg-muted/20 space-y-3 p-3">
|
|
124
|
+
{/* Repo indicator (multi-repo mode) */}
|
|
125
|
+
{repoName && (
|
|
126
|
+
<div className="text-muted-foreground flex items-center gap-1 text-xs">
|
|
127
|
+
<GitBranch className="h-3 w-3" />
|
|
128
|
+
Committing to:{" "}
|
|
129
|
+
<span className="text-foreground font-medium">{repoName}</span>
|
|
130
|
+
<span className="text-muted-foreground/70">({branch})</span>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{/* Warning for multiple repos with staged changes */}
|
|
135
|
+
{multipleReposWarning && (
|
|
136
|
+
<div className="flex items-start gap-2 rounded-md bg-yellow-500/10 px-2 py-1.5 text-xs text-yellow-600 dark:text-yellow-500">
|
|
137
|
+
<AlertTriangle className="mt-0.5 h-3 w-3 flex-shrink-0" />
|
|
138
|
+
<span>
|
|
139
|
+
Multiple repos have staged changes. Only the first will be
|
|
140
|
+
committed.
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Commit message input */}
|
|
146
|
+
<div className="space-y-1.5">
|
|
147
|
+
<label className="text-muted-foreground flex items-center gap-1 text-xs">
|
|
148
|
+
<GitCommit className="h-3 w-3" />
|
|
149
|
+
Commit message
|
|
150
|
+
</label>
|
|
151
|
+
<textarea
|
|
152
|
+
value={message}
|
|
153
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
154
|
+
placeholder="Describe your changes..."
|
|
155
|
+
rows={3}
|
|
156
|
+
className={cn(
|
|
157
|
+
"w-full resize-none rounded-md px-3 py-2 text-sm",
|
|
158
|
+
"bg-muted/50",
|
|
159
|
+
"focus:ring-primary/50 focus:ring-2 focus:outline-none",
|
|
160
|
+
"placeholder:text-muted-foreground/50"
|
|
161
|
+
)}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Error message */}
|
|
166
|
+
{error && <p className="px-1 text-xs text-red-500">{error}</p>}
|
|
167
|
+
|
|
168
|
+
{/* Success message */}
|
|
169
|
+
{success && <p className="px-1 text-xs text-green-500">{success}</p>}
|
|
170
|
+
|
|
171
|
+
{/* Buttons */}
|
|
172
|
+
<div className="flex gap-2">
|
|
173
|
+
<Button
|
|
174
|
+
variant="outline"
|
|
175
|
+
size="default"
|
|
176
|
+
onClick={handleCommit}
|
|
177
|
+
disabled={!canCommit || committing || pushing}
|
|
178
|
+
className="min-h-[44px] flex-1"
|
|
179
|
+
>
|
|
180
|
+
{committing ? (
|
|
181
|
+
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
|
182
|
+
) : (
|
|
183
|
+
<GitCommit className="mr-1 h-4 w-4" />
|
|
184
|
+
)}
|
|
185
|
+
Commit
|
|
186
|
+
</Button>
|
|
187
|
+
|
|
188
|
+
<Button
|
|
189
|
+
variant="default"
|
|
190
|
+
size="default"
|
|
191
|
+
onClick={handleCommitAndPush}
|
|
192
|
+
disabled={!canCommit || committing || pushing}
|
|
193
|
+
className="min-h-[44px] flex-1"
|
|
194
|
+
>
|
|
195
|
+
{pushing ? (
|
|
196
|
+
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
|
197
|
+
) : (
|
|
198
|
+
<Send className="mr-1 h-4 w-4" />
|
|
199
|
+
)}
|
|
200
|
+
Commit & Push
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|