@siteboon/claude-code-ui 1.8.2 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/assets/index-CeR_JfKq.js +895 -0
  2. package/dist/assets/index-Co7ALK3i.css +32 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +6 -1
  5. package/server/database/auth.db +0 -0
  6. package/.env.example +0 -12
  7. package/.nvmrc +0 -1
  8. package/postcss.config.js +0 -6
  9. package/src/App.jsx +0 -751
  10. package/src/components/ChatInterface.jsx +0 -3485
  11. package/src/components/ClaudeLogo.jsx +0 -11
  12. package/src/components/ClaudeStatus.jsx +0 -107
  13. package/src/components/CodeEditor.jsx +0 -422
  14. package/src/components/CreateTaskModal.jsx +0 -88
  15. package/src/components/CursorLogo.jsx +0 -9
  16. package/src/components/DarkModeToggle.jsx +0 -35
  17. package/src/components/DiffViewer.jsx +0 -41
  18. package/src/components/ErrorBoundary.jsx +0 -73
  19. package/src/components/FileTree.jsx +0 -480
  20. package/src/components/GitPanel.jsx +0 -1283
  21. package/src/components/ImageViewer.jsx +0 -54
  22. package/src/components/LoginForm.jsx +0 -110
  23. package/src/components/MainContent.jsx +0 -577
  24. package/src/components/MicButton.jsx +0 -272
  25. package/src/components/MobileNav.jsx +0 -88
  26. package/src/components/NextTaskBanner.jsx +0 -695
  27. package/src/components/PRDEditor.jsx +0 -871
  28. package/src/components/ProtectedRoute.jsx +0 -44
  29. package/src/components/QuickSettingsPanel.jsx +0 -262
  30. package/src/components/Settings.jsx +0 -2023
  31. package/src/components/SetupForm.jsx +0 -135
  32. package/src/components/Shell.jsx +0 -663
  33. package/src/components/Sidebar.jsx +0 -1665
  34. package/src/components/StandaloneShell.jsx +0 -106
  35. package/src/components/TaskCard.jsx +0 -210
  36. package/src/components/TaskDetail.jsx +0 -406
  37. package/src/components/TaskIndicator.jsx +0 -108
  38. package/src/components/TaskList.jsx +0 -1054
  39. package/src/components/TaskMasterSetupWizard.jsx +0 -603
  40. package/src/components/TaskMasterStatus.jsx +0 -86
  41. package/src/components/TodoList.jsx +0 -91
  42. package/src/components/Tooltip.jsx +0 -91
  43. package/src/components/ui/badge.jsx +0 -31
  44. package/src/components/ui/button.jsx +0 -46
  45. package/src/components/ui/input.jsx +0 -19
  46. package/src/components/ui/scroll-area.jsx +0 -23
  47. package/src/contexts/AuthContext.jsx +0 -158
  48. package/src/contexts/TaskMasterContext.jsx +0 -324
  49. package/src/contexts/TasksSettingsContext.jsx +0 -95
  50. package/src/contexts/ThemeContext.jsx +0 -94
  51. package/src/contexts/WebSocketContext.jsx +0 -29
  52. package/src/hooks/useAudioRecorder.js +0 -109
  53. package/src/hooks/useVersionCheck.js +0 -39
  54. package/src/index.css +0 -822
  55. package/src/lib/utils.js +0 -6
  56. package/src/main.jsx +0 -10
  57. package/src/utils/api.js +0 -141
  58. package/src/utils/websocket.js +0 -109
  59. package/src/utils/whisper.js +0 -37
  60. package/tailwind.config.js +0 -63
  61. package/vite.config.js +0 -29
  62. /package/{public → dist}/convert-icons.md +0 -0
  63. /package/{public → dist}/favicon.png +0 -0
  64. /package/{public → dist}/favicon.svg +0 -0
  65. /package/{public → dist}/generate-icons.js +0 -0
  66. /package/{public → dist}/icons/claude-ai-icon.svg +0 -0
  67. /package/{public → dist}/icons/cursor.svg +0 -0
  68. /package/{public → dist}/icons/generate-icons.md +0 -0
  69. /package/{public → dist}/icons/icon-128x128.png +0 -0
  70. /package/{public → dist}/icons/icon-128x128.svg +0 -0
  71. /package/{public → dist}/icons/icon-144x144.png +0 -0
  72. /package/{public → dist}/icons/icon-144x144.svg +0 -0
  73. /package/{public → dist}/icons/icon-152x152.png +0 -0
  74. /package/{public → dist}/icons/icon-152x152.svg +0 -0
  75. /package/{public → dist}/icons/icon-192x192.png +0 -0
  76. /package/{public → dist}/icons/icon-192x192.svg +0 -0
  77. /package/{public → dist}/icons/icon-384x384.png +0 -0
  78. /package/{public → dist}/icons/icon-384x384.svg +0 -0
  79. /package/{public → dist}/icons/icon-512x512.png +0 -0
  80. /package/{public → dist}/icons/icon-512x512.svg +0 -0
  81. /package/{public → dist}/icons/icon-72x72.png +0 -0
  82. /package/{public → dist}/icons/icon-72x72.svg +0 -0
  83. /package/{public → dist}/icons/icon-96x96.png +0 -0
  84. /package/{public → dist}/icons/icon-96x96.svg +0 -0
  85. /package/{public → dist}/icons/icon-template.svg +0 -0
  86. /package/{public → dist}/logo.svg +0 -0
  87. /package/{public → dist}/manifest.json +0 -0
  88. /package/{public → dist}/screenshots/cli-selection.png +0 -0
  89. /package/{public → dist}/screenshots/desktop-main.png +0 -0
  90. /package/{public → dist}/screenshots/mobile-chat.png +0 -0
  91. /package/{public → dist}/screenshots/tools-modal.png +0 -0
  92. /package/{public → dist}/sw.js +0 -0
@@ -1,1283 +0,0 @@
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;