mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-13 17:11:30 +00:00
feat: git panel redesign (#535)
* feat(git-panel): add Branches tab, Fetch always visible, inline error banners - Add dedicated Branches tab (local/remote sections, switch with confirmation, delete branch, create branch) - Rename History tab to Commits; add change-count badge on Changes tab - Fetch button always visible when remote exists (not only when both ahead & behind) - Inline error banner below header for failed push/pull/fetch, with dismiss button - Server: /api/git/branches now returns localBranches + remoteBranches separately - Server: add /api/git/delete-branch endpoint (prevents deleting current branch) - Controller: expose operationError, clearOperationError, deleteBranch, localBranches, remoteBranches - Constants: add deleteBranch to all ConfirmActionType record maps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: git log datetime * feat(git-panel): add staged/unstaged sections and enhanced commit details --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,6 @@ import type { ConfirmationRequest, FileStatusCode, GitDiffMap, GitStatusResponse
|
||||
import { getAllChangedFiles, hasChangedFiles } from '../../utils/gitPanelUtils';
|
||||
import CommitComposer from './CommitComposer';
|
||||
import FileChangeList from './FileChangeList';
|
||||
import FileSelectionControls from './FileSelectionControls';
|
||||
import FileStatusLegend from './FileStatusLegend';
|
||||
|
||||
type ChangesViewProps = {
|
||||
@@ -56,8 +55,12 @@ export default function ChangesView({
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserve previous behavior: every fresh status snapshot reselects changed files.
|
||||
setSelectedFiles(new Set(getAllChangedFiles(gitStatus)));
|
||||
// Remove any selected files that no longer exist in the status
|
||||
setSelectedFiles((prev) => {
|
||||
const allFiles = new Set(getAllChangedFiles(gitStatus));
|
||||
const next = new Set([...prev].filter((f) => allFiles.has(f)));
|
||||
return next;
|
||||
});
|
||||
}, [gitStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -129,6 +132,11 @@ export default function ChangesView({
|
||||
return onGenerateCommitMessage(Array.from(selectedFiles));
|
||||
}, [onGenerateCommitMessage, selectedFiles]);
|
||||
|
||||
const unstagedFiles = useMemo(
|
||||
() => new Set(changedFiles.filter((f) => !selectedFiles.has(f))),
|
||||
[changedFiles, selectedFiles],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommitComposer
|
||||
@@ -141,17 +149,6 @@ export default function ChangesView({
|
||||
onRequestConfirmation={onRequestConfirmation}
|
||||
/>
|
||||
|
||||
{gitStatus && !gitStatus.error && (
|
||||
<FileSelectionControls
|
||||
isMobile={isMobile}
|
||||
selectedCount={selectedFiles.size}
|
||||
totalCount={changedFiles.length}
|
||||
isHidden={hasExpandedFiles}
|
||||
onSelectAll={() => setSelectedFiles(new Set(changedFiles))}
|
||||
onDeselectAll={() => setSelectedFiles(new Set())}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!gitStatus?.error && <FileStatusLegend isMobile={isMobile} />}
|
||||
|
||||
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
||||
@@ -193,21 +190,71 @@ export default function ChangesView({
|
||||
</div>
|
||||
) : (
|
||||
<div className={isMobile ? 'pb-4' : ''}>
|
||||
<FileChangeList
|
||||
gitStatus={gitStatus}
|
||||
gitDiff={gitDiff}
|
||||
expandedFiles={expandedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
isMobile={isMobile}
|
||||
wrapText={wrapText}
|
||||
onToggleSelected={toggleFileSelected}
|
||||
onToggleExpanded={toggleFileExpanded}
|
||||
onOpenFile={(filePath) => {
|
||||
void onOpenFile(filePath);
|
||||
}}
|
||||
onToggleWrapText={() => onWrapTextChange(!wrapText)}
|
||||
onRequestFileAction={requestFileAction}
|
||||
/>
|
||||
{/* STAGED section */}
|
||||
<div className="flex items-center justify-between border-b border-border/60 bg-muted/30 px-3 py-1.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Staged ({selectedFiles.size})
|
||||
</span>
|
||||
{selectedFiles.size > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedFiles(new Set())}
|
||||
className="text-xs text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
Unstage All
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{selectedFiles.size === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground italic">No staged files</div>
|
||||
) : (
|
||||
<FileChangeList
|
||||
gitStatus={gitStatus}
|
||||
gitDiff={gitDiff}
|
||||
expandedFiles={expandedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
isMobile={isMobile}
|
||||
wrapText={wrapText}
|
||||
filePaths={selectedFiles}
|
||||
onToggleSelected={toggleFileSelected}
|
||||
onToggleExpanded={toggleFileExpanded}
|
||||
onOpenFile={(filePath) => { void onOpenFile(filePath); }}
|
||||
onToggleWrapText={() => onWrapTextChange(!wrapText)}
|
||||
onRequestFileAction={requestFileAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* CHANGES section */}
|
||||
<div className="flex items-center justify-between border-b border-border/60 bg-muted/30 px-3 py-1.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Changes ({unstagedFiles.size})
|
||||
</span>
|
||||
{unstagedFiles.size > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedFiles(new Set(changedFiles))}
|
||||
className="text-xs text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
Stage All
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{unstagedFiles.size === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground italic">All changes staged</div>
|
||||
) : (
|
||||
<FileChangeList
|
||||
gitStatus={gitStatus}
|
||||
gitDiff={gitDiff}
|
||||
expandedFiles={expandedFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
isMobile={isMobile}
|
||||
wrapText={wrapText}
|
||||
filePaths={unstagedFiles}
|
||||
onToggleSelected={toggleFileSelected}
|
||||
onToggleExpanded={toggleFileExpanded}
|
||||
onOpenFile={(filePath) => { void onOpenFile(filePath); }}
|
||||
onToggleWrapText={() => onWrapTextChange(!wrapText)}
|
||||
onRequestFileAction={requestFileAction}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ type FileChangeListProps = {
|
||||
selectedFiles: Set<string>;
|
||||
isMobile: boolean;
|
||||
wrapText: boolean;
|
||||
filePaths?: Set<string>;
|
||||
onToggleSelected: (filePath: string) => void;
|
||||
onToggleExpanded: (filePath: string) => void;
|
||||
onOpenFile: (filePath: string) => void;
|
||||
@@ -23,6 +24,7 @@ export default function FileChangeList({
|
||||
selectedFiles,
|
||||
isMobile,
|
||||
wrapText,
|
||||
filePaths,
|
||||
onToggleSelected,
|
||||
onToggleExpanded,
|
||||
onOpenFile,
|
||||
@@ -32,23 +34,25 @@ export default function FileChangeList({
|
||||
return (
|
||||
<>
|
||||
{FILE_STATUS_GROUPS.map(({ key, status }) =>
|
||||
(gitStatus[key] || []).map((filePath) => (
|
||||
<FileChangeItem
|
||||
key={filePath}
|
||||
filePath={filePath}
|
||||
status={status}
|
||||
isMobile={isMobile}
|
||||
isExpanded={expandedFiles.has(filePath)}
|
||||
isSelected={selectedFiles.has(filePath)}
|
||||
diff={gitDiff[filePath]}
|
||||
wrapText={wrapText}
|
||||
onToggleSelected={onToggleSelected}
|
||||
onToggleExpanded={onToggleExpanded}
|
||||
onOpenFile={onOpenFile}
|
||||
onToggleWrapText={onToggleWrapText}
|
||||
onRequestFileAction={onRequestFileAction}
|
||||
/>
|
||||
)),
|
||||
(gitStatus[key] || [])
|
||||
.filter((filePath) => !filePaths || filePaths.has(filePath))
|
||||
.map((filePath) => (
|
||||
<FileChangeItem
|
||||
key={filePath}
|
||||
filePath={filePath}
|
||||
status={status}
|
||||
isMobile={isMobile}
|
||||
isExpanded={expandedFiles.has(filePath)}
|
||||
isSelected={selectedFiles.has(filePath)}
|
||||
diff={gitDiff[filePath]}
|
||||
wrapText={wrapText}
|
||||
onToggleSelected={onToggleSelected}
|
||||
onToggleExpanded={onToggleExpanded}
|
||||
onOpenFile={onOpenFile}
|
||||
onToggleWrapText={onToggleWrapText}
|
||||
onRequestFileAction={onRequestFileAction}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user