@siteboon/claude-code-ui 1.8.2
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 +12 -0
- package/.nvmrc +1 -0
- package/LICENSE +675 -0
- package/README.md +275 -0
- package/index.html +48 -0
- package/package.json +84 -0
- package/postcss.config.js +6 -0
- package/public/convert-icons.md +53 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +9 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-128x128.svg +12 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-144x144.svg +12 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-152x152.svg +12 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-192x192.svg +12 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-384x384.svg +12 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-512x512.svg +12 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-72x72.svg +12 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/icon-96x96.svg +12 -0
- package/public/icons/icon-template.svg +12 -0
- package/public/logo.svg +9 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/cli-selection.png +0 -0
- package/public/screenshots/desktop-main.png +0 -0
- package/public/screenshots/mobile-chat.png +0 -0
- package/public/screenshots/tools-modal.png +0 -0
- package/public/sw.js +49 -0
- package/server/claude-cli.js +391 -0
- package/server/cursor-cli.js +250 -0
- package/server/database/db.js +86 -0
- package/server/database/init.sql +16 -0
- package/server/index.js +1167 -0
- package/server/middleware/auth.js +80 -0
- package/server/projects.js +1063 -0
- package/server/routes/auth.js +135 -0
- package/server/routes/cursor.js +794 -0
- package/server/routes/git.js +823 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +552 -0
- package/server/routes/taskmaster.js +1971 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/src/App.jsx +751 -0
- package/src/components/ChatInterface.jsx +3485 -0
- package/src/components/ClaudeLogo.jsx +11 -0
- package/src/components/ClaudeStatus.jsx +107 -0
- package/src/components/CodeEditor.jsx +422 -0
- package/src/components/CreateTaskModal.jsx +88 -0
- package/src/components/CursorLogo.jsx +9 -0
- package/src/components/DarkModeToggle.jsx +35 -0
- package/src/components/DiffViewer.jsx +41 -0
- package/src/components/ErrorBoundary.jsx +73 -0
- package/src/components/FileTree.jsx +480 -0
- package/src/components/GitPanel.jsx +1283 -0
- package/src/components/ImageViewer.jsx +54 -0
- package/src/components/LoginForm.jsx +110 -0
- package/src/components/MainContent.jsx +577 -0
- package/src/components/MicButton.jsx +272 -0
- package/src/components/MobileNav.jsx +88 -0
- package/src/components/NextTaskBanner.jsx +695 -0
- package/src/components/PRDEditor.jsx +871 -0
- package/src/components/ProtectedRoute.jsx +44 -0
- package/src/components/QuickSettingsPanel.jsx +262 -0
- package/src/components/Settings.jsx +2023 -0
- package/src/components/SetupForm.jsx +135 -0
- package/src/components/Shell.jsx +663 -0
- package/src/components/Sidebar.jsx +1665 -0
- package/src/components/StandaloneShell.jsx +106 -0
- package/src/components/TaskCard.jsx +210 -0
- package/src/components/TaskDetail.jsx +406 -0
- package/src/components/TaskIndicator.jsx +108 -0
- package/src/components/TaskList.jsx +1054 -0
- package/src/components/TaskMasterSetupWizard.jsx +603 -0
- package/src/components/TaskMasterStatus.jsx +86 -0
- package/src/components/TodoList.jsx +91 -0
- package/src/components/Tooltip.jsx +91 -0
- package/src/components/ui/badge.jsx +31 -0
- package/src/components/ui/button.jsx +46 -0
- package/src/components/ui/input.jsx +19 -0
- package/src/components/ui/scroll-area.jsx +23 -0
- package/src/contexts/AuthContext.jsx +158 -0
- package/src/contexts/TaskMasterContext.jsx +324 -0
- package/src/contexts/TasksSettingsContext.jsx +95 -0
- package/src/contexts/ThemeContext.jsx +94 -0
- package/src/contexts/WebSocketContext.jsx +29 -0
- package/src/hooks/useAudioRecorder.js +109 -0
- package/src/hooks/useVersionCheck.js +39 -0
- package/src/index.css +822 -0
- package/src/lib/utils.js +6 -0
- package/src/main.jsx +10 -0
- package/src/utils/api.js +141 -0
- package/src/utils/websocket.js +109 -0
- package/src/utils/whisper.js +37 -0
- package/tailwind.config.js +63 -0
- package/vite.config.js +29 -0
|
@@ -0,0 +1,1283 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle, Upload } from 'lucide-react';
|
|
3
|
+
import { MicButton } from './MicButton.jsx';
|
|
4
|
+
import { authenticatedFetch } from '../utils/api';
|
|
5
|
+
import DiffViewer from './DiffViewer.jsx';
|
|
6
|
+
|
|
7
|
+
function GitPanel({ selectedProject, isMobile }) {
|
|
8
|
+
const [gitStatus, setGitStatus] = useState(null);
|
|
9
|
+
const [gitDiff, setGitDiff] = useState({});
|
|
10
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
11
|
+
const [commitMessage, setCommitMessage] = useState('');
|
|
12
|
+
const [expandedFiles, setExpandedFiles] = useState(new Set());
|
|
13
|
+
const [selectedFiles, setSelectedFiles] = useState(new Set());
|
|
14
|
+
const [isCommitting, setIsCommitting] = useState(false);
|
|
15
|
+
const [currentBranch, setCurrentBranch] = useState('');
|
|
16
|
+
const [branches, setBranches] = useState([]);
|
|
17
|
+
const [wrapText, setWrapText] = useState(true);
|
|
18
|
+
const [showLegend, setShowLegend] = useState(false);
|
|
19
|
+
const [showBranchDropdown, setShowBranchDropdown] = useState(false);
|
|
20
|
+
const [showNewBranchModal, setShowNewBranchModal] = useState(false);
|
|
21
|
+
const [newBranchName, setNewBranchName] = useState('');
|
|
22
|
+
const [isCreatingBranch, setIsCreatingBranch] = useState(false);
|
|
23
|
+
const [activeView, setActiveView] = useState('changes'); // 'changes' or 'history'
|
|
24
|
+
const [recentCommits, setRecentCommits] = useState([]);
|
|
25
|
+
const [expandedCommits, setExpandedCommits] = useState(new Set());
|
|
26
|
+
const [commitDiffs, setCommitDiffs] = useState({});
|
|
27
|
+
const [isGeneratingMessage, setIsGeneratingMessage] = useState(false);
|
|
28
|
+
const [remoteStatus, setRemoteStatus] = useState(null);
|
|
29
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
30
|
+
const [isPulling, setIsPulling] = useState(false);
|
|
31
|
+
const [isPushing, setIsPushing] = useState(false);
|
|
32
|
+
const [isPublishing, setIsPublishing] = useState(false);
|
|
33
|
+
const [isCommitAreaCollapsed, setIsCommitAreaCollapsed] = useState(isMobile); // Collapsed by default on mobile
|
|
34
|
+
const [confirmAction, setConfirmAction] = useState(null); // { type: 'discard|commit|pull|push', file?: string, message?: string }
|
|
35
|
+
const textareaRef = useRef(null);
|
|
36
|
+
const dropdownRef = useRef(null);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (selectedProject) {
|
|
40
|
+
fetchGitStatus();
|
|
41
|
+
fetchBranches();
|
|
42
|
+
fetchRemoteStatus();
|
|
43
|
+
if (activeView === 'history') {
|
|
44
|
+
fetchRecentCommits();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, [selectedProject, activeView]);
|
|
48
|
+
|
|
49
|
+
// Handle click outside dropdown
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const handleClickOutside = (event) => {
|
|
52
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
53
|
+
setShowBranchDropdown(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
58
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const fetchGitStatus = async () => {
|
|
62
|
+
if (!selectedProject) return;
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
try {
|
|
67
|
+
const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if (data.error) {
|
|
72
|
+
console.error('Git status error:', data.error);
|
|
73
|
+
setGitStatus({ error: data.error, details: data.details });
|
|
74
|
+
} else {
|
|
75
|
+
setGitStatus(data);
|
|
76
|
+
setCurrentBranch(data.branch || 'main');
|
|
77
|
+
|
|
78
|
+
// Auto-select all changed files
|
|
79
|
+
const allFiles = new Set([
|
|
80
|
+
...(data.modified || []),
|
|
81
|
+
...(data.added || []),
|
|
82
|
+
...(data.deleted || []),
|
|
83
|
+
...(data.untracked || [])
|
|
84
|
+
]);
|
|
85
|
+
setSelectedFiles(allFiles);
|
|
86
|
+
|
|
87
|
+
// Fetch diffs for changed files
|
|
88
|
+
for (const file of data.modified || []) {
|
|
89
|
+
fetchFileDiff(file);
|
|
90
|
+
}
|
|
91
|
+
for (const file of data.added || []) {
|
|
92
|
+
fetchFileDiff(file);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Error fetching git status:', error);
|
|
97
|
+
} finally {
|
|
98
|
+
setIsLoading(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const fetchBranches = async () => {
|
|
103
|
+
try {
|
|
104
|
+
const response = await authenticatedFetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
|
|
107
|
+
if (!data.error && data.branches) {
|
|
108
|
+
setBranches(data.branches);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error fetching branches:', error);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const fetchRemoteStatus = async () => {
|
|
116
|
+
if (!selectedProject) return;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await authenticatedFetch(`/api/git/remote-status?project=${encodeURIComponent(selectedProject.name)}`);
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
|
|
122
|
+
if (!data.error) {
|
|
123
|
+
setRemoteStatus(data);
|
|
124
|
+
} else {
|
|
125
|
+
setRemoteStatus(null);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error fetching remote status:', error);
|
|
129
|
+
setRemoteStatus(null);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const switchBranch = async (branchName) => {
|
|
134
|
+
try {
|
|
135
|
+
const response = await authenticatedFetch('/api/git/checkout', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
project: selectedProject.name,
|
|
140
|
+
branch: branchName
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
if (data.success) {
|
|
146
|
+
setCurrentBranch(branchName);
|
|
147
|
+
setShowBranchDropdown(false);
|
|
148
|
+
fetchGitStatus(); // Refresh status after branch switch
|
|
149
|
+
} else {
|
|
150
|
+
console.error('Failed to switch branch:', data.error);
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error switching branch:', error);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const createBranch = async () => {
|
|
158
|
+
if (!newBranchName.trim()) return;
|
|
159
|
+
|
|
160
|
+
setIsCreatingBranch(true);
|
|
161
|
+
try {
|
|
162
|
+
const response = await authenticatedFetch('/api/git/create-branch', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
project: selectedProject.name,
|
|
167
|
+
branch: newBranchName.trim()
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
if (data.success) {
|
|
173
|
+
setCurrentBranch(newBranchName.trim());
|
|
174
|
+
setShowNewBranchModal(false);
|
|
175
|
+
setShowBranchDropdown(false);
|
|
176
|
+
setNewBranchName('');
|
|
177
|
+
fetchBranches(); // Refresh branch list
|
|
178
|
+
fetchGitStatus(); // Refresh status
|
|
179
|
+
} else {
|
|
180
|
+
console.error('Failed to create branch:', data.error);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('Error creating branch:', error);
|
|
184
|
+
} finally {
|
|
185
|
+
setIsCreatingBranch(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleFetch = async () => {
|
|
190
|
+
setIsFetching(true);
|
|
191
|
+
try {
|
|
192
|
+
const response = await authenticatedFetch('/api/git/fetch', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
project: selectedProject.name
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const data = await response.json();
|
|
201
|
+
if (data.success) {
|
|
202
|
+
// Refresh status after successful fetch
|
|
203
|
+
fetchGitStatus();
|
|
204
|
+
fetchRemoteStatus();
|
|
205
|
+
} else {
|
|
206
|
+
console.error('Fetch failed:', data.error);
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('Error fetching from remote:', error);
|
|
210
|
+
} finally {
|
|
211
|
+
setIsFetching(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handlePull = async () => {
|
|
216
|
+
setIsPulling(true);
|
|
217
|
+
try {
|
|
218
|
+
const response = await authenticatedFetch('/api/git/pull', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
project: selectedProject.name
|
|
223
|
+
})
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const data = await response.json();
|
|
227
|
+
if (data.success) {
|
|
228
|
+
// Refresh status after successful pull
|
|
229
|
+
fetchGitStatus();
|
|
230
|
+
fetchRemoteStatus();
|
|
231
|
+
} else {
|
|
232
|
+
console.error('Pull failed:', data.error);
|
|
233
|
+
// TODO: Show user-friendly error message
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Error pulling from remote:', error);
|
|
237
|
+
} finally {
|
|
238
|
+
setIsPulling(false);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const handlePush = async () => {
|
|
243
|
+
setIsPushing(true);
|
|
244
|
+
try {
|
|
245
|
+
const response = await authenticatedFetch('/api/git/push', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
project: selectedProject.name
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
if (data.success) {
|
|
255
|
+
// Refresh status after successful push
|
|
256
|
+
fetchGitStatus();
|
|
257
|
+
fetchRemoteStatus();
|
|
258
|
+
} else {
|
|
259
|
+
console.error('Push failed:', data.error);
|
|
260
|
+
// TODO: Show user-friendly error message
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('Error pushing to remote:', error);
|
|
264
|
+
} finally {
|
|
265
|
+
setIsPushing(false);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const handlePublish = async () => {
|
|
270
|
+
setIsPublishing(true);
|
|
271
|
+
try {
|
|
272
|
+
const response = await authenticatedFetch('/api/git/publish', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
project: selectedProject.name,
|
|
277
|
+
branch: currentBranch
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const data = await response.json();
|
|
282
|
+
if (data.success) {
|
|
283
|
+
// Refresh status after successful publish
|
|
284
|
+
fetchGitStatus();
|
|
285
|
+
fetchRemoteStatus();
|
|
286
|
+
} else {
|
|
287
|
+
console.error('Publish failed:', data.error);
|
|
288
|
+
// TODO: Show user-friendly error message
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('Error publishing branch:', error);
|
|
292
|
+
} finally {
|
|
293
|
+
setIsPublishing(false);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const discardChanges = async (filePath) => {
|
|
298
|
+
try {
|
|
299
|
+
const response = await authenticatedFetch('/api/git/discard', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
302
|
+
body: JSON.stringify({
|
|
303
|
+
project: selectedProject.name,
|
|
304
|
+
file: filePath
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const data = await response.json();
|
|
309
|
+
if (data.success) {
|
|
310
|
+
// Remove from selected files and refresh status
|
|
311
|
+
setSelectedFiles(prev => {
|
|
312
|
+
const newSet = new Set(prev);
|
|
313
|
+
newSet.delete(filePath);
|
|
314
|
+
return newSet;
|
|
315
|
+
});
|
|
316
|
+
fetchGitStatus();
|
|
317
|
+
} else {
|
|
318
|
+
console.error('Discard failed:', data.error);
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error('Error discarding changes:', error);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const deleteUntrackedFile = async (filePath) => {
|
|
326
|
+
try {
|
|
327
|
+
const response = await authenticatedFetch('/api/git/delete-untracked', {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
project: selectedProject.name,
|
|
332
|
+
file: filePath
|
|
333
|
+
})
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const data = await response.json();
|
|
337
|
+
if (data.success) {
|
|
338
|
+
// Remove from selected files and refresh status
|
|
339
|
+
setSelectedFiles(prev => {
|
|
340
|
+
const newSet = new Set(prev);
|
|
341
|
+
newSet.delete(filePath);
|
|
342
|
+
return newSet;
|
|
343
|
+
});
|
|
344
|
+
fetchGitStatus();
|
|
345
|
+
} else {
|
|
346
|
+
console.error('Delete failed:', data.error);
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Error deleting untracked file:', error);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const confirmAndExecute = async () => {
|
|
354
|
+
if (!confirmAction) return;
|
|
355
|
+
|
|
356
|
+
const { type, file, message } = confirmAction;
|
|
357
|
+
setConfirmAction(null);
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
switch (type) {
|
|
361
|
+
case 'discard':
|
|
362
|
+
await discardChanges(file);
|
|
363
|
+
break;
|
|
364
|
+
case 'delete':
|
|
365
|
+
await deleteUntrackedFile(file);
|
|
366
|
+
break;
|
|
367
|
+
case 'commit':
|
|
368
|
+
await handleCommit();
|
|
369
|
+
break;
|
|
370
|
+
case 'pull':
|
|
371
|
+
await handlePull();
|
|
372
|
+
break;
|
|
373
|
+
case 'push':
|
|
374
|
+
await handlePush();
|
|
375
|
+
break;
|
|
376
|
+
case 'publish':
|
|
377
|
+
await handlePublish();
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error(`Error executing ${type}:`, error);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const fetchFileDiff = async (filePath) => {
|
|
386
|
+
try {
|
|
387
|
+
const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
|
|
388
|
+
const data = await response.json();
|
|
389
|
+
|
|
390
|
+
if (!data.error && data.diff) {
|
|
391
|
+
setGitDiff(prev => ({
|
|
392
|
+
...prev,
|
|
393
|
+
[filePath]: data.diff
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error('Error fetching file diff:', error);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const fetchRecentCommits = async () => {
|
|
402
|
+
try {
|
|
403
|
+
const response = await authenticatedFetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`);
|
|
404
|
+
const data = await response.json();
|
|
405
|
+
|
|
406
|
+
if (!data.error && data.commits) {
|
|
407
|
+
setRecentCommits(data.commits);
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Error fetching commits:', error);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const fetchCommitDiff = async (commitHash) => {
|
|
415
|
+
try {
|
|
416
|
+
const response = await authenticatedFetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`);
|
|
417
|
+
const data = await response.json();
|
|
418
|
+
|
|
419
|
+
if (!data.error && data.diff) {
|
|
420
|
+
setCommitDiffs(prev => ({
|
|
421
|
+
...prev,
|
|
422
|
+
[commitHash]: data.diff
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error('Error fetching commit diff:', error);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const generateCommitMessage = async () => {
|
|
431
|
+
setIsGeneratingMessage(true);
|
|
432
|
+
try {
|
|
433
|
+
const response = await authenticatedFetch('/api/git/generate-commit-message', {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: { 'Content-Type': 'application/json' },
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
project: selectedProject.name,
|
|
438
|
+
files: Array.from(selectedFiles)
|
|
439
|
+
})
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const data = await response.json();
|
|
443
|
+
if (data.message) {
|
|
444
|
+
setCommitMessage(data.message);
|
|
445
|
+
} else {
|
|
446
|
+
console.error('Failed to generate commit message:', data.error);
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error('Error generating commit message:', error);
|
|
450
|
+
} finally {
|
|
451
|
+
setIsGeneratingMessage(false);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const toggleFileExpanded = (filePath) => {
|
|
456
|
+
setExpandedFiles(prev => {
|
|
457
|
+
const newSet = new Set(prev);
|
|
458
|
+
if (newSet.has(filePath)) {
|
|
459
|
+
newSet.delete(filePath);
|
|
460
|
+
} else {
|
|
461
|
+
newSet.add(filePath);
|
|
462
|
+
}
|
|
463
|
+
return newSet;
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const toggleCommitExpanded = (commitHash) => {
|
|
468
|
+
setExpandedCommits(prev => {
|
|
469
|
+
const newSet = new Set(prev);
|
|
470
|
+
if (newSet.has(commitHash)) {
|
|
471
|
+
newSet.delete(commitHash);
|
|
472
|
+
} else {
|
|
473
|
+
newSet.add(commitHash);
|
|
474
|
+
// Fetch diff for this commit if not already fetched
|
|
475
|
+
if (!commitDiffs[commitHash]) {
|
|
476
|
+
fetchCommitDiff(commitHash);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return newSet;
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const toggleFileSelected = (filePath) => {
|
|
484
|
+
setSelectedFiles(prev => {
|
|
485
|
+
const newSet = new Set(prev);
|
|
486
|
+
if (newSet.has(filePath)) {
|
|
487
|
+
newSet.delete(filePath);
|
|
488
|
+
} else {
|
|
489
|
+
newSet.add(filePath);
|
|
490
|
+
}
|
|
491
|
+
return newSet;
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const handleCommit = async () => {
|
|
496
|
+
if (!commitMessage.trim() || selectedFiles.size === 0) return;
|
|
497
|
+
|
|
498
|
+
setIsCommitting(true);
|
|
499
|
+
try {
|
|
500
|
+
const response = await authenticatedFetch('/api/git/commit', {
|
|
501
|
+
method: 'POST',
|
|
502
|
+
headers: { 'Content-Type': 'application/json' },
|
|
503
|
+
body: JSON.stringify({
|
|
504
|
+
project: selectedProject.name,
|
|
505
|
+
message: commitMessage,
|
|
506
|
+
files: Array.from(selectedFiles)
|
|
507
|
+
})
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
if (data.success) {
|
|
512
|
+
// Reset state after successful commit
|
|
513
|
+
setCommitMessage('');
|
|
514
|
+
setSelectedFiles(new Set());
|
|
515
|
+
fetchGitStatus();
|
|
516
|
+
fetchRemoteStatus();
|
|
517
|
+
} else {
|
|
518
|
+
console.error('Commit failed:', data.error);
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error('Error committing changes:', error);
|
|
522
|
+
} finally {
|
|
523
|
+
setIsCommitting(false);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
const getStatusLabel = (status) => {
|
|
529
|
+
switch (status) {
|
|
530
|
+
case 'M': return 'Modified';
|
|
531
|
+
case 'A': return 'Added';
|
|
532
|
+
case 'D': return 'Deleted';
|
|
533
|
+
case 'U': return 'Untracked';
|
|
534
|
+
default: return status;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const renderCommitItem = (commit) => {
|
|
539
|
+
const isExpanded = expandedCommits.has(commit.hash);
|
|
540
|
+
const diff = commitDiffs[commit.hash];
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<div key={commit.hash} className="border-b border-gray-200 dark:border-gray-700 last:border-0">
|
|
544
|
+
<div
|
|
545
|
+
className="flex items-start p-3 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
|
546
|
+
onClick={() => toggleCommitExpanded(commit.hash)}
|
|
547
|
+
>
|
|
548
|
+
<div className="mr-2 mt-1 p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
|
|
549
|
+
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
550
|
+
</div>
|
|
551
|
+
<div className="flex-1 min-w-0">
|
|
552
|
+
<div className="flex items-start justify-between gap-2">
|
|
553
|
+
<div className="flex-1 min-w-0">
|
|
554
|
+
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
555
|
+
{commit.message}
|
|
556
|
+
</p>
|
|
557
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
558
|
+
{commit.author} • {commit.date}
|
|
559
|
+
</p>
|
|
560
|
+
</div>
|
|
561
|
+
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 flex-shrink-0">
|
|
562
|
+
{commit.hash.substring(0, 7)}
|
|
563
|
+
</span>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
{isExpanded && diff && (
|
|
568
|
+
<div className="bg-gray-50 dark:bg-gray-900">
|
|
569
|
+
<div className="max-h-96 overflow-y-auto p-2">
|
|
570
|
+
<div className="text-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
|
|
571
|
+
{commit.stats}
|
|
572
|
+
</div>
|
|
573
|
+
<DiffViewer diff={diff} fileName="commit" isMobile={isMobile} wrapText={wrapText} />
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const renderFileItem = (filePath, status) => {
|
|
582
|
+
const isExpanded = expandedFiles.has(filePath);
|
|
583
|
+
const isSelected = selectedFiles.has(filePath);
|
|
584
|
+
const diff = gitDiff[filePath];
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
<div key={filePath} className="border-b border-gray-200 dark:border-gray-700 last:border-0">
|
|
588
|
+
<div className={`flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 ${isMobile ? 'px-2 py-1.5' : 'px-3 py-2'}`}>
|
|
589
|
+
<input
|
|
590
|
+
type="checkbox"
|
|
591
|
+
checked={isSelected}
|
|
592
|
+
onChange={() => toggleFileSelected(filePath)}
|
|
593
|
+
onClick={(e) => e.stopPropagation()}
|
|
594
|
+
className={`rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600 ${isMobile ? 'mr-1.5' : 'mr-2'}`}
|
|
595
|
+
/>
|
|
596
|
+
<div
|
|
597
|
+
className="flex items-center flex-1 cursor-pointer"
|
|
598
|
+
onClick={() => toggleFileExpanded(filePath)}
|
|
599
|
+
>
|
|
600
|
+
<div className={`p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded ${isMobile ? 'mr-1' : 'mr-2'}`}>
|
|
601
|
+
<ChevronRight className={`w-3 h-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} />
|
|
602
|
+
</div>
|
|
603
|
+
<span className={`flex-1 truncate ${isMobile ? 'text-xs' : 'text-sm'}`}>{filePath}</span>
|
|
604
|
+
<div className="flex items-center gap-1">
|
|
605
|
+
{(status === 'M' || status === 'D') && (
|
|
606
|
+
<button
|
|
607
|
+
onClick={(e) => {
|
|
608
|
+
e.stopPropagation();
|
|
609
|
+
setConfirmAction({
|
|
610
|
+
type: 'discard',
|
|
611
|
+
file: filePath,
|
|
612
|
+
message: `Discard all changes to "${filePath}"? This action cannot be undone.`
|
|
613
|
+
});
|
|
614
|
+
}}
|
|
615
|
+
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-red-100 dark:hover:bg-red-900 rounded text-red-600 dark:text-red-400 font-medium flex items-center gap-1`}
|
|
616
|
+
title="Discard changes"
|
|
617
|
+
>
|
|
618
|
+
<Trash2 className={`${isMobile ? 'w-3 h-3' : 'w-3 h-3'}`} />
|
|
619
|
+
{isMobile && <span>Discard</span>}
|
|
620
|
+
</button>
|
|
621
|
+
)}
|
|
622
|
+
{status === 'U' && (
|
|
623
|
+
<button
|
|
624
|
+
onClick={(e) => {
|
|
625
|
+
e.stopPropagation();
|
|
626
|
+
setConfirmAction({
|
|
627
|
+
type: 'delete',
|
|
628
|
+
file: filePath,
|
|
629
|
+
message: `Delete untracked file "${filePath}"? This action cannot be undone.`
|
|
630
|
+
});
|
|
631
|
+
}}
|
|
632
|
+
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-red-100 dark:hover:bg-red-900 rounded text-red-600 dark:text-red-400 font-medium flex items-center gap-1`}
|
|
633
|
+
title="Delete untracked file"
|
|
634
|
+
>
|
|
635
|
+
<Trash2 className={`${isMobile ? 'w-3 h-3' : 'w-3 h-3'}`} />
|
|
636
|
+
{isMobile && <span>Delete</span>}
|
|
637
|
+
</button>
|
|
638
|
+
)}
|
|
639
|
+
<span
|
|
640
|
+
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
|
641
|
+
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
|
642
|
+
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 border-green-200 dark:border-green-800' :
|
|
643
|
+
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 border-red-200 dark:border-red-800' :
|
|
644
|
+
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
|
645
|
+
}`}
|
|
646
|
+
title={getStatusLabel(status)}
|
|
647
|
+
>
|
|
648
|
+
{status}
|
|
649
|
+
</span>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
<div className={`bg-gray-50 dark:bg-gray-900 transition-all duration-400 ease-in-out overflow-hidden ${
|
|
654
|
+
isExpanded && diff
|
|
655
|
+
? 'max-h-[600px] opacity-100 translate-y-0'
|
|
656
|
+
: 'max-h-0 opacity-0 -translate-y-1'
|
|
657
|
+
}`}>
|
|
658
|
+
{/* Operation header */}
|
|
659
|
+
<div className="flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-700">
|
|
660
|
+
<div className="flex items-center gap-2">
|
|
661
|
+
<span
|
|
662
|
+
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
|
663
|
+
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
|
664
|
+
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 border-green-200 dark:border-green-800' :
|
|
665
|
+
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 border-red-200 dark:border-red-800' :
|
|
666
|
+
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
|
667
|
+
}`}
|
|
668
|
+
>
|
|
669
|
+
{status}
|
|
670
|
+
</span>
|
|
671
|
+
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
672
|
+
{getStatusLabel(status)}
|
|
673
|
+
</span>
|
|
674
|
+
</div>
|
|
675
|
+
{isMobile && (
|
|
676
|
+
<button
|
|
677
|
+
onClick={(e) => {
|
|
678
|
+
e.stopPropagation();
|
|
679
|
+
setWrapText(!wrapText);
|
|
680
|
+
}}
|
|
681
|
+
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
|
682
|
+
title={wrapText ? "Switch to horizontal scroll" : "Switch to text wrap"}
|
|
683
|
+
>
|
|
684
|
+
{wrapText ? '↔️ Scroll' : '↩️ Wrap'}
|
|
685
|
+
</button>
|
|
686
|
+
)}
|
|
687
|
+
</div>
|
|
688
|
+
<div className="max-h-96 overflow-y-auto">
|
|
689
|
+
{diff && <DiffViewer diff={diff} fileName={filePath} isMobile={isMobile} wrapText={wrapText} />}
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
if (!selectedProject) {
|
|
697
|
+
return (
|
|
698
|
+
<div className="h-full flex items-center justify-center text-gray-500 dark:text-gray-400">
|
|
699
|
+
<p>Select a project to view source control</p>
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<div className="h-full flex flex-col bg-white dark:bg-gray-900">
|
|
706
|
+
{/* Header */}
|
|
707
|
+
<div className={`flex items-center justify-between border-b border-gray-200 dark:border-gray-700 ${isMobile ? 'px-3 py-2' : 'px-4 py-3'}`}>
|
|
708
|
+
<div className="relative" ref={dropdownRef}>
|
|
709
|
+
<button
|
|
710
|
+
onClick={() => setShowBranchDropdown(!showBranchDropdown)}
|
|
711
|
+
className={`flex items-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors ${isMobile ? 'space-x-1 px-2 py-1' : 'space-x-2 px-3 py-1.5'}`}
|
|
712
|
+
>
|
|
713
|
+
<GitBranch className={`text-gray-600 dark:text-gray-400 ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} />
|
|
714
|
+
<div className="flex items-center gap-1">
|
|
715
|
+
<span className={`font-medium ${isMobile ? 'text-xs' : 'text-sm'}`}>{currentBranch}</span>
|
|
716
|
+
{/* Remote status indicators */}
|
|
717
|
+
{remoteStatus?.hasRemote && (
|
|
718
|
+
<div className="flex items-center gap-1 text-xs">
|
|
719
|
+
{remoteStatus.ahead > 0 && (
|
|
720
|
+
<span className="text-green-600 dark:text-green-400" title={`${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} ahead`}>
|
|
721
|
+
↑{remoteStatus.ahead}
|
|
722
|
+
</span>
|
|
723
|
+
)}
|
|
724
|
+
{remoteStatus.behind > 0 && (
|
|
725
|
+
<span className="text-blue-600 dark:text-blue-400" title={`${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} behind`}>
|
|
726
|
+
↓{remoteStatus.behind}
|
|
727
|
+
</span>
|
|
728
|
+
)}
|
|
729
|
+
{remoteStatus.isUpToDate && (
|
|
730
|
+
<span className="text-gray-500 dark:text-gray-400" title="Up to date with remote">
|
|
731
|
+
✓
|
|
732
|
+
</span>
|
|
733
|
+
)}
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
</div>
|
|
737
|
+
<ChevronDown className={`w-3 h-3 text-gray-500 transition-transform ${showBranchDropdown ? 'rotate-180' : ''}`} />
|
|
738
|
+
</button>
|
|
739
|
+
|
|
740
|
+
{/* Branch Dropdown */}
|
|
741
|
+
{showBranchDropdown && (
|
|
742
|
+
<div className="absolute top-full left-0 mt-1 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
|
743
|
+
<div className="py-1 max-h-64 overflow-y-auto">
|
|
744
|
+
{branches.map(branch => (
|
|
745
|
+
<button
|
|
746
|
+
key={branch}
|
|
747
|
+
onClick={() => switchBranch(branch)}
|
|
748
|
+
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
|
749
|
+
branch === currentBranch ? 'bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-700 dark:text-gray-300'
|
|
750
|
+
}`}
|
|
751
|
+
>
|
|
752
|
+
<div className="flex items-center space-x-2">
|
|
753
|
+
{branch === currentBranch && <Check className="w-3 h-3 text-green-600 dark:text-green-400" />}
|
|
754
|
+
<span className={branch === currentBranch ? 'font-medium' : ''}>{branch}</span>
|
|
755
|
+
</div>
|
|
756
|
+
</button>
|
|
757
|
+
))}
|
|
758
|
+
</div>
|
|
759
|
+
<div className="border-t border-gray-200 dark:border-gray-700 py-1">
|
|
760
|
+
<button
|
|
761
|
+
onClick={() => {
|
|
762
|
+
setShowNewBranchModal(true);
|
|
763
|
+
setShowBranchDropdown(false);
|
|
764
|
+
}}
|
|
765
|
+
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center space-x-2"
|
|
766
|
+
>
|
|
767
|
+
<Plus className="w-3 h-3" />
|
|
768
|
+
<span>Create new branch</span>
|
|
769
|
+
</button>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
)}
|
|
773
|
+
</div>
|
|
774
|
+
|
|
775
|
+
<div className={`flex items-center ${isMobile ? 'gap-1' : 'gap-2'}`}>
|
|
776
|
+
{/* Remote action buttons - smart logic based on ahead/behind status */}
|
|
777
|
+
{remoteStatus?.hasRemote && (
|
|
778
|
+
<>
|
|
779
|
+
{/* Publish button - show when branch doesn't exist on remote */}
|
|
780
|
+
{!remoteStatus?.hasUpstream && (
|
|
781
|
+
<button
|
|
782
|
+
onClick={() => setConfirmAction({
|
|
783
|
+
type: 'publish',
|
|
784
|
+
message: `Publish branch "${currentBranch}" to ${remoteStatus.remoteName}?`
|
|
785
|
+
})}
|
|
786
|
+
disabled={isPublishing}
|
|
787
|
+
className="px-2 py-1 text-xs bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50 flex items-center gap-1"
|
|
788
|
+
title={`Publish branch "${currentBranch}" to ${remoteStatus.remoteName}`}
|
|
789
|
+
>
|
|
790
|
+
<Upload className={`w-3 h-3 ${isPublishing ? 'animate-pulse' : ''}`} />
|
|
791
|
+
<span>{isPublishing ? 'Publishing...' : 'Publish'}</span>
|
|
792
|
+
</button>
|
|
793
|
+
)}
|
|
794
|
+
|
|
795
|
+
{/* Show normal push/pull buttons only if branch has upstream */}
|
|
796
|
+
{remoteStatus?.hasUpstream && !remoteStatus?.isUpToDate && (
|
|
797
|
+
<>
|
|
798
|
+
{/* Pull button - show when behind (primary action) */}
|
|
799
|
+
{remoteStatus.behind > 0 && (
|
|
800
|
+
<button
|
|
801
|
+
onClick={() => setConfirmAction({
|
|
802
|
+
type: 'pull',
|
|
803
|
+
message: `Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}?`
|
|
804
|
+
})}
|
|
805
|
+
disabled={isPulling}
|
|
806
|
+
className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center gap-1"
|
|
807
|
+
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
|
808
|
+
>
|
|
809
|
+
<Download className={`w-3 h-3 ${isPulling ? 'animate-pulse' : ''}`} />
|
|
810
|
+
<span>{isPulling ? 'Pulling...' : `Pull ${remoteStatus.behind}`}</span>
|
|
811
|
+
</button>
|
|
812
|
+
)}
|
|
813
|
+
|
|
814
|
+
{/* Push button - show when ahead (primary action when ahead only) */}
|
|
815
|
+
{remoteStatus.ahead > 0 && (
|
|
816
|
+
<button
|
|
817
|
+
onClick={() => setConfirmAction({
|
|
818
|
+
type: 'push',
|
|
819
|
+
message: `Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}?`
|
|
820
|
+
})}
|
|
821
|
+
disabled={isPushing}
|
|
822
|
+
className="px-2 py-1 text-xs bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50 flex items-center gap-1"
|
|
823
|
+
title={`Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}`}
|
|
824
|
+
>
|
|
825
|
+
<Upload className={`w-3 h-3 ${isPushing ? 'animate-pulse' : ''}`} />
|
|
826
|
+
<span>{isPushing ? 'Pushing...' : `Push ${remoteStatus.ahead}`}</span>
|
|
827
|
+
</button>
|
|
828
|
+
)}
|
|
829
|
+
|
|
830
|
+
{/* Fetch button - show when ahead only or when diverged (secondary action) */}
|
|
831
|
+
{(remoteStatus.ahead > 0 || (remoteStatus.behind > 0 && remoteStatus.ahead > 0)) && (
|
|
832
|
+
<button
|
|
833
|
+
onClick={handleFetch}
|
|
834
|
+
disabled={isFetching}
|
|
835
|
+
className="px-2 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
|
|
836
|
+
title={`Fetch from ${remoteStatus.remoteName}`}
|
|
837
|
+
>
|
|
838
|
+
<RefreshCw className={`w-3 h-3 ${isFetching ? 'animate-spin' : ''}`} />
|
|
839
|
+
<span>{isFetching ? 'Fetching...' : 'Fetch'}</span>
|
|
840
|
+
</button>
|
|
841
|
+
)}
|
|
842
|
+
</>
|
|
843
|
+
)}
|
|
844
|
+
</>
|
|
845
|
+
)}
|
|
846
|
+
|
|
847
|
+
<button
|
|
848
|
+
onClick={() => {
|
|
849
|
+
fetchGitStatus();
|
|
850
|
+
fetchBranches();
|
|
851
|
+
fetchRemoteStatus();
|
|
852
|
+
}}
|
|
853
|
+
disabled={isLoading}
|
|
854
|
+
className={`hover:bg-gray-100 dark:hover:bg-gray-800 rounded ${isMobile ? 'p-1' : 'p-1.5'}`}
|
|
855
|
+
>
|
|
856
|
+
<RefreshCw className={`${isLoading ? 'animate-spin' : ''} ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} />
|
|
857
|
+
</button>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
{/* Git Repository Not Found Message */}
|
|
862
|
+
{gitStatus?.error ? (
|
|
863
|
+
<div className="flex-1 flex flex-col items-center justify-center text-gray-500 dark:text-gray-400 px-6 py-12">
|
|
864
|
+
<GitBranch className="w-20 h-20 mb-6 opacity-30" />
|
|
865
|
+
<h3 className="text-xl font-medium mb-3 text-center">{gitStatus.error}</h3>
|
|
866
|
+
{gitStatus.details && (
|
|
867
|
+
<p className="text-sm text-center leading-relaxed mb-6 max-w-md">{gitStatus.details}</p>
|
|
868
|
+
)}
|
|
869
|
+
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 max-w-md">
|
|
870
|
+
<p className="text-sm text-blue-700 dark:text-blue-300 text-center">
|
|
871
|
+
<strong>Tip:</strong> Run <code className="bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded font-mono text-xs">git init</code> in your project directory to initialize git source control.
|
|
872
|
+
</p>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
) : (
|
|
876
|
+
<>
|
|
877
|
+
{/* Tab Navigation - Only show when git is available and no files expanded */}
|
|
878
|
+
<div className={`flex border-b border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out ${
|
|
879
|
+
expandedFiles.size === 0
|
|
880
|
+
? 'max-h-16 opacity-100 translate-y-0'
|
|
881
|
+
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
|
882
|
+
}`}>
|
|
883
|
+
<button
|
|
884
|
+
onClick={() => setActiveView('changes')}
|
|
885
|
+
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
|
886
|
+
activeView === 'changes'
|
|
887
|
+
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
|
888
|
+
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
|
889
|
+
}`}
|
|
890
|
+
>
|
|
891
|
+
<div className="flex items-center justify-center gap-2">
|
|
892
|
+
<FileText className="w-4 h-4" />
|
|
893
|
+
<span>Changes</span>
|
|
894
|
+
</div>
|
|
895
|
+
</button>
|
|
896
|
+
<button
|
|
897
|
+
onClick={() => setActiveView('history')}
|
|
898
|
+
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
|
899
|
+
activeView === 'history'
|
|
900
|
+
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
|
901
|
+
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
|
902
|
+
}`}
|
|
903
|
+
>
|
|
904
|
+
<div className="flex items-center justify-center gap-2">
|
|
905
|
+
<History className="w-4 h-4" />
|
|
906
|
+
<span>History</span>
|
|
907
|
+
</div>
|
|
908
|
+
</button>
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
{/* Changes View */}
|
|
912
|
+
{activeView === 'changes' && (
|
|
913
|
+
<>
|
|
914
|
+
{/* Mobile Commit Toggle Button / Desktop Always Visible - Hide when files expanded */}
|
|
915
|
+
<div className={`transition-all duration-300 ease-in-out ${
|
|
916
|
+
expandedFiles.size === 0
|
|
917
|
+
? 'max-h-96 opacity-100 translate-y-0'
|
|
918
|
+
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
|
919
|
+
}`}>
|
|
920
|
+
{isMobile && isCommitAreaCollapsed ? (
|
|
921
|
+
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
|
922
|
+
<button
|
|
923
|
+
onClick={() => setIsCommitAreaCollapsed(false)}
|
|
924
|
+
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
925
|
+
>
|
|
926
|
+
<GitCommit className="w-4 h-4" />
|
|
927
|
+
<span>Commit {selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''}</span>
|
|
928
|
+
<ChevronDown className="w-3 h-3" />
|
|
929
|
+
</button>
|
|
930
|
+
</div>
|
|
931
|
+
) : (
|
|
932
|
+
<>
|
|
933
|
+
{/* Commit Message Input */}
|
|
934
|
+
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
935
|
+
{/* Mobile collapse button */}
|
|
936
|
+
{isMobile && (
|
|
937
|
+
<div className="flex items-center justify-between mb-2">
|
|
938
|
+
<span className="text-sm font-medium">Commit Changes</span>
|
|
939
|
+
<button
|
|
940
|
+
onClick={() => setIsCommitAreaCollapsed(true)}
|
|
941
|
+
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
|
942
|
+
>
|
|
943
|
+
<ChevronDown className="w-4 h-4 rotate-180" />
|
|
944
|
+
</button>
|
|
945
|
+
</div>
|
|
946
|
+
)}
|
|
947
|
+
|
|
948
|
+
<div className="relative">
|
|
949
|
+
<textarea
|
|
950
|
+
ref={textareaRef}
|
|
951
|
+
value={commitMessage}
|
|
952
|
+
onChange={(e) => setCommitMessage(e.target.value)}
|
|
953
|
+
placeholder="Message (Ctrl+Enter to commit)"
|
|
954
|
+
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 resize-none pr-20"
|
|
955
|
+
rows="3"
|
|
956
|
+
onKeyDown={(e) => {
|
|
957
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
958
|
+
handleCommit();
|
|
959
|
+
}
|
|
960
|
+
}}
|
|
961
|
+
/>
|
|
962
|
+
<div className="absolute right-2 top-2 flex gap-1">
|
|
963
|
+
<button
|
|
964
|
+
onClick={generateCommitMessage}
|
|
965
|
+
disabled={selectedFiles.size === 0 || isGeneratingMessage}
|
|
966
|
+
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
967
|
+
title="Generate commit message"
|
|
968
|
+
>
|
|
969
|
+
{isGeneratingMessage ? (
|
|
970
|
+
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
971
|
+
) : (
|
|
972
|
+
<Sparkles className="w-4 h-4" />
|
|
973
|
+
)}
|
|
974
|
+
</button>
|
|
975
|
+
<div style={{ display: 'none' }}>
|
|
976
|
+
<MicButton
|
|
977
|
+
onTranscript={(transcript) => setCommitMessage(transcript)}
|
|
978
|
+
mode="default"
|
|
979
|
+
className="p-1.5"
|
|
980
|
+
/>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
<div className="flex items-center justify-between mt-2">
|
|
985
|
+
<span className="text-xs text-gray-500">
|
|
986
|
+
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
|
987
|
+
</span>
|
|
988
|
+
<button
|
|
989
|
+
onClick={() => setConfirmAction({
|
|
990
|
+
type: 'commit',
|
|
991
|
+
message: `Commit ${selectedFiles.size} file${selectedFiles.size !== 1 ? 's' : ''} with message: "${commitMessage.trim()}"?`
|
|
992
|
+
})}
|
|
993
|
+
disabled={!commitMessage.trim() || selectedFiles.size === 0 || isCommitting}
|
|
994
|
+
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
|
995
|
+
>
|
|
996
|
+
<Check className="w-3 h-3" />
|
|
997
|
+
<span>{isCommitting ? 'Committing...' : 'Commit'}</span>
|
|
998
|
+
</button>
|
|
999
|
+
</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
</>
|
|
1002
|
+
)}
|
|
1003
|
+
</div>
|
|
1004
|
+
</>
|
|
1005
|
+
)}
|
|
1006
|
+
|
|
1007
|
+
{/* File Selection Controls - Only show in changes view and when git is working and no files expanded */}
|
|
1008
|
+
{activeView === 'changes' && gitStatus && !gitStatus.error && (
|
|
1009
|
+
<div className={`border-b border-gray-200 dark:border-gray-700 flex items-center justify-between transition-all duration-300 ease-in-out ${isMobile ? 'px-3 py-1.5' : 'px-4 py-2'} ${
|
|
1010
|
+
expandedFiles.size === 0
|
|
1011
|
+
? 'max-h-16 opacity-100 translate-y-0'
|
|
1012
|
+
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
|
1013
|
+
}`}>
|
|
1014
|
+
<span className={`text-gray-600 dark:text-gray-400 ${isMobile ? 'text-xs' : 'text-xs'}`}>
|
|
1015
|
+
{selectedFiles.size} of {(gitStatus?.modified?.length || 0) + (gitStatus?.added?.length || 0) + (gitStatus?.deleted?.length || 0) + (gitStatus?.untracked?.length || 0)} {isMobile ? '' : 'files'} selected
|
|
1016
|
+
</span>
|
|
1017
|
+
<div className={`flex ${isMobile ? 'gap-1' : 'gap-2'}`}>
|
|
1018
|
+
<button
|
|
1019
|
+
onClick={() => {
|
|
1020
|
+
const allFiles = new Set([
|
|
1021
|
+
...(gitStatus?.modified || []),
|
|
1022
|
+
...(gitStatus?.added || []),
|
|
1023
|
+
...(gitStatus?.deleted || []),
|
|
1024
|
+
...(gitStatus?.untracked || [])
|
|
1025
|
+
]);
|
|
1026
|
+
setSelectedFiles(allFiles);
|
|
1027
|
+
}}
|
|
1028
|
+
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 ${isMobile ? 'text-xs' : 'text-xs'}`}
|
|
1029
|
+
>
|
|
1030
|
+
{isMobile ? 'All' : 'Select All'}
|
|
1031
|
+
</button>
|
|
1032
|
+
<span className="text-gray-300 dark:text-gray-600">|</span>
|
|
1033
|
+
<button
|
|
1034
|
+
onClick={() => setSelectedFiles(new Set())}
|
|
1035
|
+
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 ${isMobile ? 'text-xs' : 'text-xs'}`}
|
|
1036
|
+
>
|
|
1037
|
+
{isMobile ? 'None' : 'Deselect All'}
|
|
1038
|
+
</button>
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
)}
|
|
1042
|
+
|
|
1043
|
+
{/* Status Legend Toggle - Hide on mobile by default */}
|
|
1044
|
+
{!gitStatus?.error && !isMobile && (
|
|
1045
|
+
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
1046
|
+
<button
|
|
1047
|
+
onClick={() => setShowLegend(!showLegend)}
|
|
1048
|
+
className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 text-xs text-gray-600 dark:text-gray-400 flex items-center justify-center gap-1"
|
|
1049
|
+
>
|
|
1050
|
+
<Info className="w-3 h-3" />
|
|
1051
|
+
<span>File Status Guide</span>
|
|
1052
|
+
{showLegend ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
1053
|
+
</button>
|
|
1054
|
+
|
|
1055
|
+
{showLegend && (
|
|
1056
|
+
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-800 text-xs">
|
|
1057
|
+
<div className={`${isMobile ? 'grid grid-cols-2 gap-3 justify-items-center' : 'flex justify-center gap-6'}`}>
|
|
1058
|
+
<div className="flex items-center gap-2">
|
|
1059
|
+
<span className="inline-flex items-center justify-center w-5 h-5 bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 rounded border border-yellow-200 dark:border-yellow-800 font-bold text-xs">
|
|
1060
|
+
M
|
|
1061
|
+
</span>
|
|
1062
|
+
<span className="text-gray-600 dark:text-gray-400 italic">Modified</span>
|
|
1063
|
+
</div>
|
|
1064
|
+
<div className="flex items-center gap-2">
|
|
1065
|
+
<span className="inline-flex items-center justify-center w-5 h-5 bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 rounded border border-green-200 dark:border-green-800 font-bold text-xs">
|
|
1066
|
+
A
|
|
1067
|
+
</span>
|
|
1068
|
+
<span className="text-gray-600 dark:text-gray-400 italic">Added</span>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div className="flex items-center gap-2">
|
|
1071
|
+
<span className="inline-flex items-center justify-center w-5 h-5 bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 rounded border border-red-200 dark:border-red-800 font-bold text-xs">
|
|
1072
|
+
D
|
|
1073
|
+
</span>
|
|
1074
|
+
<span className="text-gray-600 dark:text-gray-400 italic">Deleted</span>
|
|
1075
|
+
</div>
|
|
1076
|
+
<div className="flex items-center gap-2">
|
|
1077
|
+
<span className="inline-flex items-center justify-center w-5 h-5 bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 rounded border border-gray-300 dark:border-gray-600 font-bold text-xs">
|
|
1078
|
+
U
|
|
1079
|
+
</span>
|
|
1080
|
+
<span className="text-gray-600 dark:text-gray-400 italic">Untracked</span>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
</div>
|
|
1086
|
+
)}
|
|
1087
|
+
</>
|
|
1088
|
+
)}
|
|
1089
|
+
|
|
1090
|
+
{/* File List - Changes View - Only show when git is available */}
|
|
1091
|
+
{activeView === 'changes' && !gitStatus?.error && (
|
|
1092
|
+
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-20' : ''}`}>
|
|
1093
|
+
{isLoading ? (
|
|
1094
|
+
<div className="flex items-center justify-center h-32">
|
|
1095
|
+
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
|
1096
|
+
</div>
|
|
1097
|
+
) : !gitStatus || (!gitStatus.modified?.length && !gitStatus.added?.length && !gitStatus.deleted?.length && !gitStatus.untracked?.length) ? (
|
|
1098
|
+
<div className="flex flex-col items-center justify-center h-32 text-gray-500 dark:text-gray-400">
|
|
1099
|
+
<GitCommit className="w-12 h-12 mb-2 opacity-50" />
|
|
1100
|
+
<p className="text-sm">No changes detected</p>
|
|
1101
|
+
</div>
|
|
1102
|
+
) : (
|
|
1103
|
+
<div className={isMobile ? 'pb-4' : ''}>
|
|
1104
|
+
{gitStatus.modified?.map(file => renderFileItem(file, 'M'))}
|
|
1105
|
+
{gitStatus.added?.map(file => renderFileItem(file, 'A'))}
|
|
1106
|
+
{gitStatus.deleted?.map(file => renderFileItem(file, 'D'))}
|
|
1107
|
+
{gitStatus.untracked?.map(file => renderFileItem(file, 'U'))}
|
|
1108
|
+
</div>
|
|
1109
|
+
)}
|
|
1110
|
+
</div>
|
|
1111
|
+
)}
|
|
1112
|
+
|
|
1113
|
+
{/* History View - Only show when git is available */}
|
|
1114
|
+
{activeView === 'history' && !gitStatus?.error && (
|
|
1115
|
+
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-20' : ''}`}>
|
|
1116
|
+
{isLoading ? (
|
|
1117
|
+
<div className="flex items-center justify-center h-32">
|
|
1118
|
+
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
|
1119
|
+
</div>
|
|
1120
|
+
) : recentCommits.length === 0 ? (
|
|
1121
|
+
<div className="flex flex-col items-center justify-center h-32 text-gray-500 dark:text-gray-400">
|
|
1122
|
+
<History className="w-12 h-12 mb-2 opacity-50" />
|
|
1123
|
+
<p className="text-sm">No commits found</p>
|
|
1124
|
+
</div>
|
|
1125
|
+
) : (
|
|
1126
|
+
<div className={isMobile ? 'pb-4' : ''}>
|
|
1127
|
+
{recentCommits.map(commit => renderCommitItem(commit))}
|
|
1128
|
+
</div>
|
|
1129
|
+
)}
|
|
1130
|
+
</div>
|
|
1131
|
+
)}
|
|
1132
|
+
|
|
1133
|
+
{/* New Branch Modal */}
|
|
1134
|
+
{showNewBranchModal && (
|
|
1135
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
1136
|
+
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowNewBranchModal(false)} />
|
|
1137
|
+
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
|
1138
|
+
<div className="p-6">
|
|
1139
|
+
<h3 className="text-lg font-semibold mb-4">Create New Branch</h3>
|
|
1140
|
+
<div className="mb-4">
|
|
1141
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
1142
|
+
Branch Name
|
|
1143
|
+
</label>
|
|
1144
|
+
<input
|
|
1145
|
+
type="text"
|
|
1146
|
+
value={newBranchName}
|
|
1147
|
+
onChange={(e) => setNewBranchName(e.target.value)}
|
|
1148
|
+
onKeyDown={(e) => {
|
|
1149
|
+
if (e.key === 'Enter' && !isCreatingBranch) {
|
|
1150
|
+
createBranch();
|
|
1151
|
+
}
|
|
1152
|
+
}}
|
|
1153
|
+
placeholder="feature/new-feature"
|
|
1154
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
1155
|
+
autoFocus
|
|
1156
|
+
/>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 mb-4">
|
|
1159
|
+
This will create a new branch from the current branch ({currentBranch})
|
|
1160
|
+
</div>
|
|
1161
|
+
<div className="flex justify-end space-x-3">
|
|
1162
|
+
<button
|
|
1163
|
+
onClick={() => {
|
|
1164
|
+
setShowNewBranchModal(false);
|
|
1165
|
+
setNewBranchName('');
|
|
1166
|
+
}}
|
|
1167
|
+
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
|
|
1168
|
+
>
|
|
1169
|
+
Cancel
|
|
1170
|
+
</button>
|
|
1171
|
+
<button
|
|
1172
|
+
onClick={createBranch}
|
|
1173
|
+
disabled={!newBranchName.trim() || isCreatingBranch}
|
|
1174
|
+
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
|
1175
|
+
>
|
|
1176
|
+
{isCreatingBranch ? (
|
|
1177
|
+
<>
|
|
1178
|
+
<RefreshCw className="w-3 h-3 animate-spin" />
|
|
1179
|
+
<span>Creating...</span>
|
|
1180
|
+
</>
|
|
1181
|
+
) : (
|
|
1182
|
+
<>
|
|
1183
|
+
<Plus className="w-3 h-3" />
|
|
1184
|
+
<span>Create Branch</span>
|
|
1185
|
+
</>
|
|
1186
|
+
)}
|
|
1187
|
+
</button>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
</div>
|
|
1191
|
+
</div>
|
|
1192
|
+
)}
|
|
1193
|
+
|
|
1194
|
+
{/* Confirmation Modal */}
|
|
1195
|
+
{confirmAction && (
|
|
1196
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
1197
|
+
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setConfirmAction(null)} />
|
|
1198
|
+
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
|
1199
|
+
<div className="p-6">
|
|
1200
|
+
<div className="flex items-center mb-4">
|
|
1201
|
+
<div className={`p-2 rounded-full mr-3 ${
|
|
1202
|
+
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'bg-red-100 dark:bg-red-900' : 'bg-yellow-100 dark:bg-yellow-900'
|
|
1203
|
+
}`}>
|
|
1204
|
+
<AlertTriangle className={`w-5 h-5 ${
|
|
1205
|
+
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'text-red-600 dark:text-red-400' : 'text-yellow-600 dark:text-yellow-400'
|
|
1206
|
+
}`} />
|
|
1207
|
+
</div>
|
|
1208
|
+
<h3 className="text-lg font-semibold">
|
|
1209
|
+
{confirmAction.type === 'discard' ? 'Discard Changes' :
|
|
1210
|
+
confirmAction.type === 'delete' ? 'Delete File' :
|
|
1211
|
+
confirmAction.type === 'commit' ? 'Confirm Commit' :
|
|
1212
|
+
confirmAction.type === 'pull' ? 'Confirm Pull' :
|
|
1213
|
+
confirmAction.type === 'publish' ? 'Publish Branch' : 'Confirm Push'}
|
|
1214
|
+
</h3>
|
|
1215
|
+
</div>
|
|
1216
|
+
|
|
1217
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
|
1218
|
+
{confirmAction.message}
|
|
1219
|
+
</p>
|
|
1220
|
+
|
|
1221
|
+
<div className="flex justify-end space-x-3">
|
|
1222
|
+
<button
|
|
1223
|
+
onClick={() => setConfirmAction(null)}
|
|
1224
|
+
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
|
|
1225
|
+
>
|
|
1226
|
+
Cancel
|
|
1227
|
+
</button>
|
|
1228
|
+
<button
|
|
1229
|
+
onClick={confirmAndExecute}
|
|
1230
|
+
className={`px-4 py-2 text-sm text-white rounded-md ${
|
|
1231
|
+
(confirmAction.type === 'discard' || confirmAction.type === 'delete')
|
|
1232
|
+
? 'bg-red-600 hover:bg-red-700'
|
|
1233
|
+
: confirmAction.type === 'commit'
|
|
1234
|
+
? 'bg-blue-600 hover:bg-blue-700'
|
|
1235
|
+
: confirmAction.type === 'pull'
|
|
1236
|
+
? 'bg-green-600 hover:bg-green-700'
|
|
1237
|
+
: confirmAction.type === 'publish'
|
|
1238
|
+
? 'bg-purple-600 hover:bg-purple-700'
|
|
1239
|
+
: 'bg-orange-600 hover:bg-orange-700'
|
|
1240
|
+
} flex items-center space-x-2`}
|
|
1241
|
+
>
|
|
1242
|
+
{confirmAction.type === 'discard' ? (
|
|
1243
|
+
<>
|
|
1244
|
+
<Trash2 className="w-4 h-4" />
|
|
1245
|
+
<span>Discard</span>
|
|
1246
|
+
</>
|
|
1247
|
+
) : confirmAction.type === 'delete' ? (
|
|
1248
|
+
<>
|
|
1249
|
+
<Trash2 className="w-4 h-4" />
|
|
1250
|
+
<span>Delete</span>
|
|
1251
|
+
</>
|
|
1252
|
+
) : confirmAction.type === 'commit' ? (
|
|
1253
|
+
<>
|
|
1254
|
+
<Check className="w-4 h-4" />
|
|
1255
|
+
<span>Commit</span>
|
|
1256
|
+
</>
|
|
1257
|
+
) : confirmAction.type === 'pull' ? (
|
|
1258
|
+
<>
|
|
1259
|
+
<Download className="w-4 h-4" />
|
|
1260
|
+
<span>Pull</span>
|
|
1261
|
+
</>
|
|
1262
|
+
) : confirmAction.type === 'publish' ? (
|
|
1263
|
+
<>
|
|
1264
|
+
<Upload className="w-4 h-4" />
|
|
1265
|
+
<span>Publish</span>
|
|
1266
|
+
</>
|
|
1267
|
+
) : (
|
|
1268
|
+
<>
|
|
1269
|
+
<Upload className="w-4 h-4" />
|
|
1270
|
+
<span>Push</span>
|
|
1271
|
+
</>
|
|
1272
|
+
)}
|
|
1273
|
+
</button>
|
|
1274
|
+
</div>
|
|
1275
|
+
</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
)}
|
|
1279
|
+
</div>
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
export default GitPanel;
|