feat: reorganize git view layout and add history/branch actions (#354)

* feat(ui): redesigned Git view layout

* feat: stabilize git views layout with min-h-0 and scroll

- Introduce min-h-0 and flex-1 on git layout containers
- Apply min-h-0 on PR checks dialog content and related areas
- Configure ScrollableOverlay to disable horizontal scroll and overscroll
This commit is contained in:
Bohdan Triapitsyn
2026-02-08 12:16:39 +02:00
committed by GitHub
parent b432437b02
commit 0a425cd882
14 changed files with 770 additions and 509 deletions
+191 -70
View File
@@ -15,11 +15,20 @@ import {
useIsGitRepo,
} from '@/stores/useGitStore';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { RiGitBranchLine, RiLoader4Line } from '@remixicon/react';
import {
RiGitBranchLine,
RiGitMergeLine,
RiGitCommitLine,
RiGitPullRequestLine,
RiLoader4Line,
RiSplitCellsHorizontal,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { AnimatedTabs } from '@/components/ui/animated-tabs';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
@@ -46,7 +55,7 @@ import { PullRequestSection } from './git/PullRequestSection';
import { ConflictDialog } from './git/ConflictDialog';
import { StashDialog } from './git/StashDialog';
import { InProgressOperationBanner } from './git/InProgressOperationBanner';
import type { OperationLogEntry } from './git/BranchIntegrationSection';
import { BranchIntegrationSection, type OperationLogEntry } from './git/BranchIntegrationSection';
import type { GitRemote } from '@/lib/gitApi';
import { BranchPickerDialog } from '@/components/session/BranchPickerDialog';
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
@@ -54,6 +63,7 @@ import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
type SyncAction = 'fetch' | 'pull' | 'push' | null;
type CommitAction = 'commit' | 'commitAndPush' | null;
type BranchOperation = 'merge' | 'rebase' | null;
type ActionTab = 'commit' | 'branch' | 'pr' | 'worktree';
type GitViewSnapshot = {
@@ -235,8 +245,6 @@ export const GitView: React.FC = () => {
const [isBranchPickerOpen, setIsBranchPickerOpen] = React.useState(false);
const [rootBranchHint, setRootBranchHint] = React.useState<string | null>(null);
const baseBranch = worktreeMetadata?.createdFromBranch || status?.current || 'HEAD';
React.useEffect(() => {
const projectRoot = worktreeMetadata?.projectDirectory;
if (!projectRoot) {
@@ -363,6 +371,8 @@ export const GitView: React.FC = () => {
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(null);
const [gitmojiEmojis, setGitmojiEmojis] = React.useState<GitmojiEntry[]>([]);
const [gitmojiSearch, setGitmojiSearch] = React.useState('');
const [isHistoryDialogOpen, setIsHistoryDialogOpen] = React.useState(false);
const [actionTab, setActionTab] = React.useState<ActionTab>('commit');
const [remotes, setRemotes] = React.useState<GitRemote[]>([]);
const [branchOperation, setBranchOperation] = React.useState<BranchOperation>(null);
const [operationLogs, setOperationLogs] = React.useState<OperationLogEntry[]>([]);
@@ -915,6 +925,21 @@ export const GitView: React.FC = () => {
.sort();
}, [branches]);
const baseBranch = React.useMemo(() => {
const fromMeta = typeof worktreeMetadata?.createdFromBranch === 'string'
? worktreeMetadata.createdFromBranch.trim()
: '';
if (fromMeta && fromMeta !== 'HEAD') return fromMeta;
const fromHint = typeof rootBranchHint === 'string' ? rootBranchHint.trim() : '';
if (fromHint && fromHint !== 'HEAD') return fromHint;
if (localBranches.includes('main')) return 'main';
if (localBranches.includes('master')) return 'master';
if (localBranches.includes('develop')) return 'develop';
return 'main';
}, [localBranches, rootBranchHint, worktreeMetadata?.createdFromBranch]);
const availableIdentities = React.useMemo(() => {
const unique = new Map<string, GitIdentityProfile>();
if (globalIdentity) {
@@ -998,6 +1023,29 @@ export const GitView: React.FC = () => {
const selectedCount = selectedPaths.size;
const isBusy = isLoading || syncAction !== null || commitAction !== null;
const hasChanges = uniqueChangeCount > 0;
const canShowIntegrateCommitsSection = Boolean(
worktreeMetadata && repoRootForIntegrate && sourceBranchForIntegrate && shouldShowIntegrateCommits
);
const canShowPullRequestSection = Boolean(
currentDirectory && status?.current && status?.tracking && status.current !== baseBranch
);
const canShowBranchWorkflows = Boolean(status?.current);
const integrateCommitsProps =
canShowIntegrateCommitsSection && repoRootForIntegrate && sourceBranchForIntegrate && worktreeMetadata
? {
repoRoot: repoRootForIntegrate,
sourceBranch: sourceBranchForIntegrate,
worktreeMetadata,
}
: null;
const pullRequestProps =
canShowPullRequestSection && currentDirectory && status?.current
? {
directory: currentDirectory,
branch: status.current,
}
: null;
// Keep these sections stable in layout; individual cards render placeholders when unavailable.
const toggleFileSelection = (path: string) => {
setSelectedPaths((previous) => {
@@ -1484,12 +1532,7 @@ export const GitView: React.FC = () => {
onSelectIdentity={handleApplyIdentity}
isApplyingIdentity={isSettingIdentity}
isWorktreeMode={!!worktreeMetadata}
onMerge={handleMerge}
onRebase={handleRebase}
branchOperation={branchOperation}
operationLogs={operationLogs}
onOperationComplete={handleOperationComplete}
isBusy={isBusy}
onOpenHistory={() => setIsHistoryDialogOpen(true)}
onOpenBranchPicker={branchPickerProject ? () => setIsBranchPickerOpen(true) : undefined}
/>
@@ -1509,12 +1552,12 @@ export const GitView: React.FC = () => {
/>
)}
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="p-3">
<div className="flex flex-col gap-3">
{/* Two-column layout on large screens: Changes + Commit */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div className="flex-1 min-h-0 overflow-hidden">
<div className="h-full min-h-0 grid grid-cols-1 xl:grid-cols-[minmax(520px,1fr)_480px]">
<div className="min-w-0 min-h-0 h-full flex flex-col">
{hasChanges ? (
<ChangesSection
variant="plain"
changeEntries={changeEntries}
selectedPaths={selectedPaths}
diffStats={status?.diffStats}
@@ -1526,7 +1569,7 @@ export const GitView: React.FC = () => {
onRevertFile={handleRevertFile}
/>
) : (
<div className="lg:col-span-2 flex justify-center">
<div className="flex-1 min-h-0 flex items-center justify-center px-6">
<GitEmptyState
behind={status?.behind ?? 0}
onPull={() => {
@@ -1540,66 +1583,144 @@ export const GitView: React.FC = () => {
/>
</div>
)}
{changeEntries.length > 0 && (
<CommitSection
selectedCount={selectedCount}
commitMessage={commitMessage}
onCommitMessageChange={setCommitMessage}
generatedHighlights={generatedHighlights}
onInsertHighlights={handleInsertHighlights}
onClearHighlights={clearGeneratedHighlights}
onGenerateMessage={handleGenerateCommitMessage}
isGeneratingMessage={isGeneratingMessage}
onCommit={() => handleCommit({ pushAfter: false })}
onCommitAndPush={() => handleCommit({ pushAfter: true })}
commitAction={commitAction}
isBusy={isBusy}
gitmojiEnabled={settingsGitmojiEnabled}
onOpenGitmojiPicker={() => setIsGitmojiPickerOpen(true)}
/>
)}
</div>
{worktreeMetadata && repoRootForIntegrate && sourceBranchForIntegrate && shouldShowIntegrateCommits ? (
<IntegrateCommitsSection
repoRoot={repoRootForIntegrate}
sourceBranch={sourceBranchForIntegrate}
worktreeMetadata={worktreeMetadata}
localBranches={localBranches}
defaultTargetBranch={defaultTargetBranch}
refreshKey={integrateRefreshKey}
onRefresh={() => {
if (!currentDirectory) return;
fetchStatus(currentDirectory, git);
fetchBranches(currentDirectory, git);
fetchLog(currentDirectory, git, logMaxCountLocal);
}}
/>
) : null}
<div className="min-w-0 min-h-0 h-full border-t xl:border-t-0 xl:border-l border-border/40 bg-muted/10 flex flex-col">
<div className="px-3 py-3">
<AnimatedTabs<ActionTab>
value={actionTab}
onValueChange={setActionTab}
collapseLabelsOnSmall
tabs={[
{ value: 'commit', label: 'Commit', icon: RiGitCommitLine },
{ value: 'branch', label: 'Update branch', icon: RiGitMergeLine },
{ value: 'pr', label: 'PR', icon: RiGitPullRequestLine },
{ value: 'worktree', label: 'Worktree', icon: RiSplitCellsHorizontal },
]}
/>
</div>
<div className="h-px bg-border/40" />
{currentDirectory && status?.current && status?.tracking ? (
<PullRequestSection
directory={currentDirectory}
branch={status.current}
baseBranch={baseBranch}
/>
) : null}
<ScrollableOverlay
outerClassName="flex-1 min-h-0"
className="px-4 py-4"
disableHorizontal
preventOverscroll
>
{actionTab === 'commit' ? (
<CommitSection
variant="plain"
selectedCount={selectedCount}
commitMessage={commitMessage}
onCommitMessageChange={setCommitMessage}
generatedHighlights={generatedHighlights}
onInsertHighlights={handleInsertHighlights}
onClearHighlights={clearGeneratedHighlights}
onGenerateMessage={handleGenerateCommitMessage}
isGeneratingMessage={isGeneratingMessage}
onCommit={() => handleCommit({ pushAfter: false })}
onCommitAndPush={() => handleCommit({ pushAfter: true })}
commitAction={commitAction}
isBusy={isBusy}
gitmojiEnabled={settingsGitmojiEnabled}
onOpenGitmojiPicker={() => setIsGitmojiPickerOpen(true)}
/>
) : null}
{/* History below, constrained width */}
<HistorySection
log={log}
isLogLoading={isLogLoading}
logMaxCount={logMaxCountLocal}
onLogMaxCountChange={handleLogMaxCountChange}
expandedCommitHashes={expandedCommitHashes}
onToggleCommit={handleToggleCommit}
commitFilesMap={commitFilesMap}
loadingCommitHashes={loadingCommitHashes}
onCopyHash={handleCopyCommitHash}
/>
{actionTab === 'branch' ? (
<div className="space-y-4">
{canShowBranchWorkflows ? (
<BranchIntegrationSection
mode="inline"
currentBranch={status?.current}
localBranches={localBranches}
remoteBranches={remoteBranches}
onMerge={handleMerge}
onRebase={handleRebase}
disabled={isBusy}
isOperating={branchOperation !== null}
operationLogs={operationLogs}
onOperationComplete={handleOperationComplete}
/>
) : (
<p className="typography-meta text-muted-foreground">Branch actions unavailable.</p>
)}
</div>
) : null}
{actionTab === 'worktree' ? (
integrateCommitsProps ? (
<IntegrateCommitsSection
variant="plain"
repoRoot={integrateCommitsProps.repoRoot}
sourceBranch={integrateCommitsProps.sourceBranch}
worktreeMetadata={integrateCommitsProps.worktreeMetadata}
localBranches={localBranches}
defaultTargetBranch={defaultTargetBranch}
refreshKey={integrateRefreshKey}
onRefresh={() => {
if (!currentDirectory) return;
fetchStatus(currentDirectory, git);
fetchBranches(currentDirectory, git);
fetchLog(currentDirectory, git, logMaxCountLocal);
}}
/>
) : (
<div className="space-y-1">
<div className="typography-ui-header font-semibold text-foreground">Re-integrate commits</div>
<div className="typography-micro text-muted-foreground">
Available in worktree mode.
</div>
</div>
)
) : null}
{actionTab === 'pr' ? (
pullRequestProps ? (
<PullRequestSection
variant="plain"
directory={pullRequestProps.directory}
branch={pullRequestProps.branch}
baseBranch={baseBranch}
/>
) : (
<div className="space-y-1">
<div className="typography-ui-header font-semibold text-foreground">Pull Request</div>
<div className="typography-micro text-muted-foreground">
Push a non-base branch (with upstream) to create a PR.
</div>
</div>
)
) : null}
</ScrollableOverlay>
</div>
</div>
</ScrollableOverlay>
</div>
<Dialog open={isHistoryDialogOpen} onOpenChange={setIsHistoryDialogOpen}>
<DialogContent className="max-w-5xl max-h-[80vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>History</DialogTitle>
<DialogDescription>
Browse recent commits and inspect file-level changes.
</DialogDescription>
</DialogHeader>
<div className="flex-1 min-h-0">
<HistorySection
log={log}
isLogLoading={isLogLoading}
logMaxCount={logMaxCountLocal}
onLogMaxCountChange={handleLogMaxCountChange}
expandedCommitHashes={expandedCommitHashes}
onToggleCommit={handleToggleCommit}
commitFilesMap={commitFilesMap}
loadingCommitHashes={loadingCommitHashes}
onCopyHash={handleCopyCommitHash}
showHeader={false}
/>
</div>
</DialogContent>
</Dialog>
<Dialog open={isGitmojiPickerOpen} onOpenChange={setIsGitmojiPickerOpen}>
<DialogContent className="max-w-md p-0 overflow-hidden">