Major UI refresh: sidebar redesign, theme expansion, and chat performance optimizations (#706)

## Summary
Complete sidebar redesign and comprehensive UI polish pass with performance optimizations, theme system refinements, and desktop integration improvements.

## Key Changes

**Sidebar & Navigation Redesign**
- Redesigned sessions sidebar layout with unified button primitives
- Added activity sections with project grouping and improved session organization
- Refined sidebar corners, spacing, and visual hierarchy
- Removed NavRail component in favor of streamlined sidebar
- Stabilized sessions bar toggle position in fullscreen mode

**Performance Optimizations**
- Reduced chat streaming CPU usage and storage churn
- Optimized task tool polling and live timers with debouncing
- Prevented chat state races and reduced background request load
- Debounced draft writes and coalesced session reloads
- Optimized message store updates and turn tracking

**Theme & Visual System**
- Added theme-aware window corners (desktop) and border radius tokens
- Introduced glassmorphism effects on desktop sidebar
- Added backdrop blur to UI elements

**Chat Experience**
- Added session-based permission auto-accept toggle in chat input
- Polished permission shield UX with improved icon sizing and spacing
- Fixed chat scroll-to-bottom behavior and timeline tracking
- Enhanced tool output display with better path label detection
- Removed duplicate draft context details in chat header
- Added text selection menu to chat messages

**Git Improvements**
- Refreshed git history visual design with cleaner dividers
- Added remote removal action in sync selector
- Stabilized git polling to prevent excessive requests
- Improved tool output rendering for git operations

**Settings & Panels**
- Fixed mobile scrolling on settings pages
- Made outside-click settings close instantly
- Reduced settings load churn and CPU spikes
- Improved services dropdown layout and spacing
- Softened panel resize handles

**Desktop Integration**
- Synced macOS window theme with app theme
- Restored window dragging in sidebar header zones
- Fixed system window corners on macOS
- Improved header session metadata and action controls

**Button & Component Standardization**
- Unified button primitives across all components
- Standardized destructive action patterns
- Removed unused button variants (button-large, button-small)
- Aligned context tab close hit areas

---------

Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
This commit is contained in:
Bohdan Triapitsyn
2026-03-20 01:01:03 +02:00
committed by GitHub
co-authored by Iuliia Ivashko
parent 359879153a
commit 321cc7252a
222 changed files with 8575 additions and 5456 deletions
+206 -368
View File
@@ -461,7 +461,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const searchInputRef = React.useRef<HTMLInputElement>(null);
const [showMobilePageContent, setShowMobilePageContent] = React.useState(false);
const [wrapLines, setWrapLines] = React.useState(isMobile);
const [wrapLines, setWrapLines] = React.useState(true);
const [isFullscreen, setIsFullscreen] = React.useState(false);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const [textViewMode, setTextViewMode] = React.useState<'view' | 'edit'>('edit');
@@ -2114,6 +2114,202 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
);
}, [currentTheme.metadata.variant, pierreTheme, wrapLines]);
const renderFloatingFileControls = ({ exitFullscreenOnly = false }: { exitFullscreenOnly?: boolean } = {}) => {
if (!selectedFile) {
return null;
}
return (
<div className="pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/95 p-1 shadow-sm backdrop-blur-sm">
{canEdit && textViewMode === 'edit' && (
isSaving ? (
<span className="flex items-center gap-1 px-1 text-muted-foreground typography-meta">
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
Saving...
</span>
) : autoSaveStatus === 'saved' && !isDirty ? (
<span className="flex items-center gap-1 px-1 text-[color:var(--status-success)] typography-meta">
<RiCheckLine className="h-3.5 w-3.5" />
Saved
</span>
) : isDirty ? (
<Button
variant="ghost"
size="sm"
onClick={() => void saveDraft()}
className="h-6 px-1 gap-1 text-muted-foreground opacity-80 hover:opacity-100"
title={`Save now (${getModifierLabel()}+S) - auto-saves after 1.5s`}
aria-label={`Save (${getModifierLabel()}+S)`}
>
<RiSave3Line className="h-4 w-4" />
</Button>
) : null
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground opacity-80 hover:opacity-100"
title="Open in desktop app"
aria-label="Open in desktop app"
>
<RiFileTransferLine className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 max-h-[70vh] overflow-y-auto">
{openInApps.map((app) => (
<DropdownMenuItem
key={app.id}
className="flex items-center gap-2"
onClick={() => void handleOpenInApp(app)}
>
<OpenInAppListIcon label={app.label} iconDataUrl={app.iconDataUrl} />
<span className="typography-ui-label text-foreground">{app.label}</span>
</DropdownMenuItem>
))}
{openInCacheStale ? (
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => void loadOpenInApps(true)}
>
<RiRefreshLine className="h-4 w-4" />
<span className="typography-ui-label text-foreground">Refresh Apps</span>
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
{!isSelectedImage && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setWrapLines(!wrapLines)}
className={cn(
'h-6 w-6 p-0 transition-opacity',
wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
)}
title={wrapLines ? 'Disable line wrap' : 'Enable line wrap'}
>
<RiTextWrap className="size-4" />
</Button>
{textViewMode === 'edit' && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsSearchOpen(!isSearchOpen)}
className={cn(
'h-6 w-6 p-0 transition-opacity',
isSearchOpen ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
)}
title="Find in file"
>
<RiSearchLine className="size-4" />
</Button>
)}
</>
)}
{isMarkdown && (
<PreviewToggleButton
currentMode={getMdViewMode()}
onToggle={() => saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview')}
/>
)}
{canCopy && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
const result = await copyTextToClipboard(fileContent);
if (result.ok) {
setCopiedContent(true);
if (copiedContentTimeoutRef.current !== null) {
window.clearTimeout(copiedContentTimeoutRef.current);
}
copiedContentTimeoutRef.current = window.setTimeout(() => {
setCopiedContent(false);
}, 1200);
} else {
toast.error('Copy failed');
}
}}
className="h-6 w-6 p-0"
title="Copy file contents"
aria-label="Copy file contents"
>
{copiedContent ? (
<RiCheckLine className="h-4 w-4 text-[color:var(--status-success)]" />
) : (
<RiClipboardLine className="h-4 w-4" />
)}
</Button>
)}
{canCopyPath && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
const result = await copyTextToClipboard(displaySelectedPath);
if (result.ok) {
setCopiedPath(true);
if (copiedPathTimeoutRef.current !== null) {
window.clearTimeout(copiedPathTimeoutRef.current);
}
copiedPathTimeoutRef.current = window.setTimeout(() => {
setCopiedPath(false);
}, 1200);
} else {
toast.error('Copy failed');
}
}}
className="h-6 w-6 p-0"
title={`Copy file path (${displaySelectedPath})`}
aria-label={`Copy file path (${displaySelectedPath})`}
>
{copiedPath ? (
<RiCheckLine className="h-4 w-4 text-[color:var(--status-success)]" />
) : (
<RiFileCopy2Line className="h-4 w-4" />
)}
</Button>
)}
{exitFullscreenOnly ? (
<Button
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(false)}
className="h-6 w-6 p-0"
title="Exit fullscreen"
aria-label="Exit fullscreen"
>
<RiFullscreenExitLine className="h-4 w-4" />
</Button>
) : (!isMobile && mode === 'full' && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(!isFullscreen)}
className="h-6 w-6 p-0"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
>
{isFullscreen ? (
<RiFullscreenExitLine className="h-4 w-4" />
) : (
<RiFullscreenLine className="h-4 w-4" />
)}
</Button>
))}
</div>
);
};
const fileViewer = (
<div
className="relative flex h-full min-h-0 min-w-0 w-full flex-col overflow-hidden"
@@ -2144,7 +2340,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
</DialogFooter>
</DialogContent>
</Dialog>
<div className="flex flex-col border-b border-border/40 flex-shrink-0">
<div className={cn('flex flex-col flex-shrink-0', showEditorTabsRow && 'border-b border-border/40')}>
{/* Row 1: Tabs */}
{showEditorTabsRow ? (
<div className="flex min-w-0 items-center px-3 py-1.5">
@@ -2288,199 +2484,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
</div>
) : null}
{/* Row 2: Actions (right-aligned) */}
{selectedFile && (
<div className={cn('flex items-center justify-end gap-1 px-3 pb-1.5', !showEditorTabsRow && 'pt-1.5')}>
{canEdit && textViewMode === 'edit' && (
isSaving ? (
<span className="flex items-center gap-1 text-muted-foreground typography-meta">
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
Saving
</span>
) : autoSaveStatus === 'saved' && !isDirty ? (
<span className="flex items-center gap-1 text-[color:var(--status-success)] typography-meta">
<RiCheckLine className="h-3.5 w-3.5" />
Saved
</span>
) : isDirty ? (
<Button
variant="ghost"
size="sm"
onClick={() => void saveDraft()}
className="h-5 px-1 gap-1 text-muted-foreground opacity-70 hover:opacity-100"
title={`Save now (${getModifierLabel()}+S) — auto-saves after 1.5s`}
aria-label={`Save (${getModifierLabel()}+S)`}
>
<RiSave3Line className="h-3.5 w-3.5" />
</Button>
) : null
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 text-muted-foreground opacity-70 hover:opacity-100"
title="Open in desktop app"
aria-label="Open in desktop app"
>
<RiFileTransferLine className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 max-h-[70vh] overflow-y-auto">
{openInApps.map((app) => (
<DropdownMenuItem
key={app.id}
className="flex items-center gap-2"
onClick={() => void handleOpenInApp(app)}
>
<OpenInAppListIcon label={app.label} iconDataUrl={app.iconDataUrl} />
<span className="typography-ui-label text-foreground">{app.label}</span>
</DropdownMenuItem>
))}
{openInCacheStale ? (
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => void loadOpenInApps(true)}
>
<RiRefreshLine className="h-4 w-4" />
<span className="typography-ui-label text-foreground">Refresh Apps</span>
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
{canEdit && !isSelectedImage && (
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
)}
{!isSelectedImage && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setWrapLines(!wrapLines)}
className={cn(
'h-5 w-5 p-0 transition-opacity',
wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-60 hover:opacity-100'
)}
title={wrapLines ? 'Disable line wrap' : 'Enable line wrap'}
>
<RiTextWrap className="size-4" />
</Button>
{textViewMode === 'edit' && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsSearchOpen(!isSearchOpen)}
className={cn(
'h-5 w-5 p-0 transition-opacity',
isSearchOpen ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-60 hover:opacity-100'
)}
title="Find in file"
>
<RiSearchLine className="size-4" />
</Button>
)}
</>
)}
{(canCopy || canCopyPath || isMarkdown) && (canEdit || !isSelectedImage) && (
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
)}
{isMarkdown && (
<PreviewToggleButton
currentMode={getMdViewMode()}
onToggle={() => saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview')}
/>
)}
{canCopy && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
const result = await copyTextToClipboard(fileContent);
if (result.ok) {
setCopiedContent(true);
if (copiedContentTimeoutRef.current !== null) {
window.clearTimeout(copiedContentTimeoutRef.current);
}
copiedContentTimeoutRef.current = window.setTimeout(() => {
setCopiedContent(false);
}, 1200);
} else {
toast.error('Copy failed');
}
}}
className="h-5 w-5 p-0"
title="Copy file contents"
aria-label="Copy file contents"
>
{copiedContent ? (
<RiCheckLine className="h-4 w-4 text-[color:var(--status-success)]" />
) : (
<RiClipboardLine className="h-4 w-4" />
)}
</Button>
)}
{canCopyPath && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
const result = await copyTextToClipboard(displaySelectedPath);
if (result.ok) {
setCopiedPath(true);
if (copiedPathTimeoutRef.current !== null) {
window.clearTimeout(copiedPathTimeoutRef.current);
}
copiedPathTimeoutRef.current = window.setTimeout(() => {
setCopiedPath(false);
}, 1200);
} else {
toast.error('Copy failed');
}
}}
className="h-5 w-5 p-0"
title={`Copy file path (${displaySelectedPath})`}
aria-label={`Copy file path (${displaySelectedPath})`}
>
{copiedPath ? (
<RiCheckLine className="h-4 w-4 text-[color:var(--status-success)]" />
) : (
<RiFileCopy2Line className="h-4 w-4" />
)}
</Button>
)}
{!isMobile && mode === 'full' && (
<>
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
<Button
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(!isFullscreen)}
className="h-5 w-5 p-0"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
>
{isFullscreen ? (
<RiFullscreenExitLine className="h-4 w-4" />
) : (
<RiFullscreenLine className="h-4 w-4" />
)}
</Button>
</>
)}
</div>
)}
</div>
<div className="flex-1 min-h-0 min-w-0 relative">
{selectedFile && !isSearchOpen && (
<div className="absolute right-3 top-3 z-30">
{renderFloatingFileControls()}
</div>
)}
<ScrollableOverlay outerClassName="h-full min-w-0" className="h-full min-w-0">
{!selectedFile ? (
<div className="p-3 typography-ui text-muted-foreground">Pick a file from the tree.</div>
@@ -2747,184 +2758,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
// Fullscreen file viewer overlay
const fullscreenViewer = mode === 'full' && isFullscreen && selectedFile && (
<div className="absolute inset-0 z-50 flex flex-col bg-background">
{/* Fullscreen header */}
<div className="flex min-w-0 items-center gap-2 border-b border-border/40 px-4 py-2 flex-shrink-0">
<div className="min-w-0 flex-1">
<div className="typography-ui-label font-medium truncate">
{selectedFile.name}
</div>
<div className="typography-meta text-muted-foreground truncate" title={displaySelectedPath}>
{displaySelectedPath}
</div>
</div>
<div className="flex items-center gap-1">
{canEdit && textViewMode === 'edit' && (
isSaving ? (
<span className="flex items-center gap-1 text-muted-foreground typography-meta">
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
Saving
</span>
) : autoSaveStatus === 'saved' && !isDirty ? (
<span className="flex items-center gap-1 text-[color:var(--status-success)] typography-meta">
<RiCheckLine className="h-3.5 w-3.5" />
Saved
</span>
) : isDirty ? (
<Button
variant="ghost"
size="sm"
onClick={() => void saveDraft()}
className="h-6 px-1 gap-1 text-muted-foreground opacity-70 hover:opacity-100"
title={`Save now (${getModifierLabel()}+S) — auto-saves after 1.5s`}
aria-label={`Save (${getModifierLabel()}+S)`}
>
<RiSave3Line className="h-4 w-4" />
</Button>
) : null
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground opacity-70 hover:opacity-100"
title="Open in desktop app"
aria-label="Open in desktop app"
>
<RiFileTransferLine className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 max-h-[70vh] overflow-y-auto">
{openInApps.map((app) => (
<DropdownMenuItem
key={app.id}
className="flex items-center gap-2"
onClick={() => void handleOpenInApp(app)}
>
<OpenInAppListIcon label={app.label} iconDataUrl={app.iconDataUrl} />
<span className="typography-ui-label text-foreground">{app.label}</span>
</DropdownMenuItem>
))}
{openInCacheStale ? (
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => void loadOpenInApps(true)}
>
<RiRefreshLine className="h-4 w-4" />
<span className="typography-ui-label text-foreground">Refresh Apps</span>
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
{canEdit && !isSelectedImage && (
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
)}
{!isSelectedImage && (
<Button
variant="ghost"
size="sm"
onClick={() => setWrapLines(!wrapLines)}
className={cn(
'h-6 w-6 p-0 transition-opacity',
wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-60 hover:opacity-100'
)}
title={wrapLines ? 'Disable line wrap' : 'Enable line wrap'}
>
<RiTextWrap className="size-4" />
</Button>
)}
{(canCopy || canCopyPath || isMarkdown) && (canEdit || !isSelectedImage) && (
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
)}
{isMarkdown && (
<PreviewToggleButton
currentMode={getMdViewMode()}
onToggle={() => saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview')}
/>
)}
{canCopy && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
const result = await copyTextToClipboard(fileContent);
if (result.ok) {
setCopiedContent(true);
if (copiedContentTimeoutRef.current !== null) {
window.clearTimeout(copiedContentTimeoutRef.current);
}
copiedContentTimeoutRef.current = window.setTimeout(() => {
setCopiedContent(false);
}, 1200);
} else {
toast.error('Copy failed');
}
}}
className="h-6 w-6 p-0"
title="Copy file contents"
aria-label="Copy file contents"
>
{copiedContent ? (
<RiCheckLine className="h-4 w-4 text-[color:var(--status-success)]" />
) : (
<RiClipboardLine className="h-4 w-4" />
)}
</Button>
)}
{canCopyPath && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
const result = await copyTextToClipboard(displaySelectedPath);
if (result.ok) {
setCopiedPath(true);
if (copiedPathTimeoutRef.current !== null) {
window.clearTimeout(copiedPathTimeoutRef.current);
}
copiedPathTimeoutRef.current = window.setTimeout(() => {
setCopiedPath(false);
}, 1200);
} else {
toast.error('Copy failed');
}
}}
className="h-6 w-6 p-0"
title={`Copy file path (${displaySelectedPath})`}
aria-label={`Copy file path (${displaySelectedPath})`}
>
{copiedPath ? (
<RiCheckLine className="h-4 w-4 text-[color:var(--status-success)]" />
) : (
<RiFileCopy2Line className="h-4 w-4" />
)}
</Button>
)}
<span aria-hidden="true" className="mx-1 h-4 w-px bg-border/60" />
<Button
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(false)}
className="h-6 w-6 p-0"
title="Exit fullscreen"
aria-label="Exit fullscreen"
>
<RiFullscreenExitLine className="h-4 w-4" />
</Button>
</div>
</div>
{/* Fullscreen content */}
<div className="flex-1 min-h-0 min-w-0 relative">
<div className="absolute right-4 top-4 z-30">
{renderFloatingFileControls({ exitFullscreenOnly: true })}
</div>
<ScrollableOverlay outerClassName="h-full min-w-0" className="h-full min-w-0">
{fileLoading ? (
suppressFileLoadingIndicator
+53 -7
View File
@@ -433,6 +433,7 @@ export const GitView: React.FC = () => {
return isActionTab(stored) ? stored : 'commit';
});
const [remotes, setRemotes] = React.useState<GitRemote[]>([]);
const [removingRemoteName, setRemovingRemoteName] = React.useState<string | null>(null);
const [branchOperation, setBranchOperation] = React.useState<BranchOperation>(null);
const [operationLogs, setOperationLogs] = React.useState<OperationLogEntry[]>([]);
const [conflictDialogOpen, setConflictDialogOpen] = React.useState(false);
@@ -585,14 +586,23 @@ export const GitView: React.FC = () => {
git.getRemoteUrl(currentDirectory).then(setRemoteUrl).catch(() => setRemoteUrl(null));
}, [currentDirectory, git]);
React.useEffect(() => {
const refreshRemotes = React.useCallback(async () => {
if (!currentDirectory || !git?.getRemotes) {
setRemotes([]);
return;
}
git.getRemotes(currentDirectory).then(setRemotes).catch(() => setRemotes([]));
try {
const remoteList = await git.getRemotes(currentDirectory);
setRemotes(remoteList);
} catch {
setRemotes([]);
}
}, [currentDirectory, git]);
React.useEffect(() => {
void refreshRemotes();
}, [refreshRemotes]);
React.useEffect(() => {
if (!settingsGitmojiEnabled) {
setGitmojiEmojis([]);
@@ -825,6 +835,35 @@ export const GitView: React.FC = () => {
}
};
const handleRemoveRemote = React.useCallback(async (remote: GitRemote) => {
if (!currentDirectory) return;
const remoteName = remote.name.trim();
if (!remoteName) {
toast.error('Remote name is required');
return;
}
if (remoteName === 'origin') {
toast.error('Cannot remove origin remote');
return;
}
setRemovingRemoteName(remoteName);
try {
await git.removeRemote(currentDirectory, { remote: remoteName });
toast.success(`Removed ${remoteName} remote`);
await Promise.all([
refreshStatusAndBranches(false),
refreshRemotes(),
]);
} catch (error) {
const message = error instanceof Error ? error.message : `Failed to remove ${remoteName}`;
toast.error(message);
} finally {
setRemovingRemoteName(null);
}
}, [currentDirectory, git, refreshRemotes, refreshStatusAndBranches]);
const handleCommit = async (options: { pushAfter?: boolean } = {}) => {
if (!currentDirectory) return;
if (!commitMessage.trim()) {
@@ -1254,11 +1293,20 @@ export const GitView: React.FC = () => {
}
const insertBeforeIndex = log.all.findIndex((entry) => !branchHashes.has(entry.hash));
if (insertBeforeIndex <= 0) {
if (insertBeforeIndex === 0) {
setHistoryBranchDivider(null);
return;
}
if (insertBeforeIndex === -1) {
setHistoryBranchDivider({
insertBeforeIndex: log.all.length,
branchName: currentBranch,
direction: 'up',
});
return;
}
setHistoryBranchDivider({
insertBeforeIndex,
branchName: currentBranch,
@@ -1806,6 +1854,8 @@ export const GitView: React.FC = () => {
onFetch={(remote) => handleSyncAction('fetch', remote)}
onPull={(remote) => handleSyncAction('pull', remote)}
onPush={() => handleSyncAction('push')}
onRemoveRemote={handleRemoveRemote}
removingRemoteName={removingRemoteName}
onCheckoutBranch={handleCheckoutBranch}
onCreateBranch={handleCreateBranch}
onRenameBranch={handleRenameBranch}
@@ -1861,7 +1911,6 @@ export const GitView: React.FC = () => {
{(changeEntries?.length ?? 0) > 0 ? (
<>
<ChangesSection
variant="plain"
maxListHeightClassName="max-h-[40vh]"
changeEntries={changeEntries}
onVisiblePathsChange={setVisibleChangePaths}
@@ -1887,7 +1936,6 @@ export const GitView: React.FC = () => {
/>
<CommitSection
variant="plain"
selectedCount={selectedCount}
commitMessage={commitMessage}
onCommitMessageChange={setCommitMessage}
@@ -1945,7 +1993,6 @@ export const GitView: React.FC = () => {
<div className="space-y-4">
{integrateCommitsProps ? (
<IntegrateCommitsSection
variant="plain"
repoRoot={integrateCommitsProps.repoRoot}
sourceBranch={integrateCommitsProps.sourceBranch}
worktreeMetadata={integrateCommitsProps.worktreeMetadata}
@@ -1974,7 +2021,6 @@ export const GitView: React.FC = () => {
<div className="space-y-4">
{pullRequestProps ? (
<PullRequestSection
variant="plain"
directory={pullRequestProps.directory}
branch={pullRequestProps.branch}
baseBranch={baseBranch}
@@ -240,6 +240,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const isMobile = forceMobile ?? deviceInfo.isMobile;
const settingsPageRaw = useUIStore((state) => state.settingsPage);
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const settingsSlug = resolveSettingsSlug(settingsPageRaw);
@@ -322,25 +323,27 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
// Load stores when project changes or when a page becomes active.
React.useEffect(() => {
if (!isSettingsDialogOpen && !runtimeCtx.isVSCode) {
return;
}
if (settingsSlug === 'agents') {
setTimeout(() => void useAgentsStore.getState().loadAgents(), 0);
void useAgentsStore.getState().loadAgents();
return;
}
if (settingsSlug === 'commands') {
setTimeout(() => void useCommandsStore.getState().loadCommands(), 0);
void useCommandsStore.getState().loadCommands();
return;
}
if (settingsSlug === 'mcp') {
setTimeout(() => void useMcpConfigStore.getState().loadMcpConfigs(), 0);
void useMcpConfigStore.getState().loadMcpConfigs();
return;
}
if (settingsSlug === 'skills.installed' || settingsSlug === 'skills.catalog') {
setTimeout(() => {
void useSkillsStore.getState().loadSkills();
void useSkillsCatalogStore.getState().loadCatalog();
}, 0);
void useSkillsStore.getState().loadSkills();
void useSkillsCatalogStore.getState().loadCatalog();
}
}, [activeProjectId, settingsSlug]);
}, [activeProjectId, isSettingsDialogOpen, runtimeCtx.isVSCode, settingsSlug]);
const openPage = React.useCallback((slug: SettingsPageSlug) => {
setSettingsPage(slug);
@@ -579,7 +582,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const renderMobileStage = () => {
if (mobileStage === 'nav') {
return (
<div className={cn('flex-1 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<div className="flex h-full min-h-0 flex-col">
<ErrorBoundary>{renderSettingsNav(false)}</ErrorBoundary>
</div>
@@ -596,13 +599,13 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
// No sidebar available; fall back to direct content.
const fallback = renderPageContent(settingsSlug);
return (
<div className="flex-1 overflow-hidden bg-background" data-keyboard-avoid="true">
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
<ErrorBoundary>{fallback}</ErrorBoundary>
</div>
);
}
return (
<div className={cn('flex-1 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<ErrorBoundary>
{renderPageSidebar(settingsSlug, { onItemSelect: () => setMobileStage('page-content') })}
</ErrorBoundary>
@@ -614,7 +617,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const content = renderPageContent(settingsSlug);
return (
<div className="flex-1 overflow-hidden bg-background" data-keyboard-avoid="true">
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
<ErrorBoundary>{content}</ErrorBoundary>
</div>
);
@@ -646,7 +649,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
};
return (
<div ref={containerRef} data-settings-view="true" className={cn('relative flex h-full flex-col overflow-hidden bg-background')}>
<div ref={containerRef} data-settings-view="true" className={cn('relative flex h-full min-h-0 flex-col overflow-hidden bg-background')}>
{isMobile ? (
<div
className={cn(
@@ -724,7 +727,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
</>
)}
<div className="flex flex-1 overflow-hidden">
<div className="flex flex-1 min-h-0 overflow-hidden">
{isMobile ? (
renderMobileStage()
) : (
@@ -733,7 +736,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
className={cn(
'relative flex h-full min-h-0 flex-col overflow-hidden border-r',
isDesktopApp
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
? 'bg-[color:var(--sidebar-overlay-strong)] supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
: runtimeCtx.isVSCode
? 'bg-background'
: 'bg-sidebar',
@@ -14,7 +14,6 @@ interface SettingsWindowProps {
*/
export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChange }) => {
const descriptionId = React.useId();
const skipNextOverlayClickRef = React.useRef(false);
const hasOpenFloatingMenu = React.useCallback(() => {
if (typeof document === 'undefined') {
@@ -31,15 +30,8 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-md"
onPointerDown={() => {
skipNextOverlayClickRef.current = hasOpenFloatingMenu();
}}
onClick={(event) => {
onPointerDown={(event) => {
event.stopPropagation();
if (skipNextOverlayClickRef.current) {
skipNextOverlayClickRef.current = false;
return;
}
if (hasOpenFloatingMenu()) {
return;
}
@@ -228,7 +228,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="h-10 w-10 flex-shrink-0" aria-label="Worktree actions">
<Button variant="outline" size="icon" className="flex-shrink-0" aria-label="Worktree actions">
<RiMore2Line className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -198,13 +198,13 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
{operationCompleted ? (
mode === 'dialog' ? (
<DialogFooter>
<Button variant="default" size="sm" className="h-7 px-2 py-0" onClick={handleClose}>
<Button variant="default" size="sm" onClick={handleClose}>
{hasError ? 'Close' : 'Done'}
</Button>
</DialogFooter>
) : (
<div className="flex justify-end">
<Button variant="default" size="sm" className="h-7 px-2 py-0" onClick={handleClose}>
<Button variant="default" size="sm" onClick={handleClose}>
{hasError ? 'Close' : 'Done'}
</Button>
</div>
@@ -284,7 +284,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
</p>
<DropdownMenu open={branchDropdownOpen} onOpenChange={setBranchDropdownOpen} modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-between h-10">
<Button variant="outline" size="lg" className="w-full justify-between">
<span className={cn('truncate', !selectedBranch && 'text-muted-foreground')}>
{selectedBranch || 'Select a branch...'}
</span>
@@ -353,7 +353,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
{mode === 'dialog' ? (
<DialogFooter className="gap-2 pt-1">
<Button variant="ghost" size="sm" className="h-7 px-2 py-0" onClick={handleCancel}>
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel
</Button>
<Button
@@ -361,7 +361,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
size="sm"
onClick={handleConfirm}
disabled={!selectedBranch}
className="h-7 px-2 py-0 gap-1.5"
className="gap-1.5"
>
{operation === 'merge' ? (
<>
@@ -378,11 +378,11 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
</DialogFooter>
) : (
<div className="flex items-center gap-2 pt-1">
<Button variant="ghost" size="sm" className="h-7 px-2 py-0" onClick={handleCancel} disabled={isDisabled}>
<Button variant="destructive" size="sm" onClick={handleCancel} disabled={isDisabled}>
Reset
</Button>
<div className="flex-1" />
<Button variant="default" size="sm" className="h-7 px-2 py-0" onClick={handleConfirm} disabled={isDisabled || !selectedBranch}>
<Button variant="default" size="sm" onClick={handleConfirm} disabled={isDisabled || !selectedBranch}>
{operation === 'merge' ? 'Merge' : 'Rebase'}
</Button>
</div>
@@ -416,7 +416,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
<Button
variant="outline"
size="sm"
className="h-7 px-2 py-0 gap-1.5"
className="gap-1.5"
onClick={handleOpenDialog}
disabled={isDisabled}
>
@@ -28,7 +28,6 @@ interface ChangesSectionProps {
onViewDiff: (path: string) => void;
onRevertFile: (path: string) => void;
isRevertingAll?: boolean;
variant?: 'framed' | 'plain';
maxListHeightClassName?: string;
onVisiblePathsChange?: (paths: string[]) => void;
}
@@ -48,7 +47,6 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
onViewDiff,
onRevertFile,
isRevertingAll = false,
variant = 'framed',
maxListHeightClassName,
onVisiblePathsChange,
}) => {
@@ -92,19 +90,10 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
onVisiblePathsChange(virtualRows.map((row) => changeEntries[row.index]?.path).filter((value): value is string => Boolean(value)));
}, [changeEntries, onVisiblePathsChange, shouldVirtualize, totalCount, virtualRows]);
const containerClassName =
variant === 'framed'
? 'flex flex-col rounded-xl border border-border/60 bg-background/70'
: 'flex flex-col flex-1 min-h-0';
const headerClassName =
variant === 'framed'
? 'flex items-center justify-between gap-2 px-3 py-2 border-b border-border/40'
: 'flex items-center justify-between gap-2 px-0 py-3 border-b border-border/40';
const scrollOuterClassName =
variant === 'framed'
? 'flex-1 min-h-0 max-h-[30vh]'
: `flex-1 min-h-0 pr-0 ${maxListHeightClassName ?? ''}`.trim();
const rowPaddingClassName = variant === 'plain' ? 'pl-0 pr-2' : 'px-3';
const containerClassName = 'flex flex-col flex-1 min-h-0';
const headerClassName = 'flex items-center justify-between gap-2 px-0 py-3 border-b border-border/40';
const scrollOuterClassName = `flex-1 min-h-0 pr-0 ${maxListHeightClassName ?? ''}`.trim();
const rowPaddingClassName = 'pl-0 pr-2';
const handleConfirmRevertAll = React.useCallback(async () => {
if (!onRevertAll || isRevertingAll || changeEntries.length === 0) {
@@ -144,12 +133,11 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
</button>
) : null}
</div>
<div className={cn('flex items-center gap-2', variant === 'plain' && 'pr-1')}>
<div className="flex items-center gap-2 pr-1">
{totalCount > 0 && onRevertAll ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-[var(--status-error)] hover:text-[var(--status-error)]"
variant="destructive"
size="xs"
onClick={() => setConfirmRevertAllOpen(true)}
disabled={isRevertingAll}
>
@@ -237,10 +225,10 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmRevertAllOpen(false)} disabled={isRevertingAll}>
<Button variant="outline" size="sm" onClick={() => setConfirmRevertAllOpen(false)} disabled={isRevertingAll}>
Cancel
</Button>
<Button variant="destructive" onClick={() => void handleConfirmRevertAll()} disabled={isRevertingAll}>
<Button variant="destructive" size="sm" onClick={() => void handleConfirmRevertAll()} disabled={isRevertingAll}>
{isRevertingAll ? 'Reverting...' : 'Revert all'}
</Button>
</DialogFooter>
@@ -5,12 +5,7 @@ import {
RiLoader4Line,
RiEmotionHappyLine,
} from '@remixicon/react';
import {
Collapsible,
CollapsibleContent,
} from '@/components/ui/collapsible';
import { Button } from '@/components/ui/button';
import { ButtonLarge } from '@/components/ui/button-large';
import { CommitInput } from './CommitInput';
import { AIHighlightsBox } from './AIHighlightsBox';
import { useDeviceInfo } from '@/lib/device';
@@ -33,7 +28,6 @@ interface CommitSectionProps {
isBusy: boolean;
gitmojiEnabled: boolean;
onOpenGitmojiPicker: () => void;
variant?: 'framed' | 'plain';
}
export const CommitSection: React.FC<CommitSectionProps> = ({
@@ -51,167 +45,148 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
isBusy,
gitmojiEnabled,
onOpenGitmojiPicker,
variant = 'framed',
}) => {
const hasSelectedFiles = selectedCount > 0;
const canCommit = commitMessage.trim() && hasSelectedFiles && commitAction === null;
const { isMobile, hasTouchInput } = useDeviceInfo();
const containerClassName =
variant === 'framed'
? 'rounded-xl border border-border/60 bg-background/70 overflow-hidden'
: 'border-0 bg-transparent rounded-none';
const headerClassName =
variant === 'framed'
? 'flex w-full items-center justify-between px-3 py-2'
: 'flex w-full items-center justify-between px-0 py-3 border-b border-border/40';
const contentClassName =
variant === 'framed'
? 'flex flex-col gap-3 p-3 pt-0'
: 'flex flex-col gap-3 px-0 py-3';
const containerClassName = 'border-0 bg-transparent rounded-none';
const headerClassName = 'flex w-full items-center justify-between px-0 pt-2 pb-1';
const contentClassName = 'flex flex-col gap-3 px-0 pt-1 pb-3';
return (
<Collapsible
open={variant === 'plain' ? true : hasSelectedFiles}
className={containerClassName}
data-keyboard-avoid="true"
>
<section className={containerClassName} data-keyboard-avoid="true">
<div className={headerClassName}>
<h3 className="typography-ui-header font-semibold text-foreground">Commit</h3>
<span className="typography-meta text-muted-foreground">
{hasSelectedFiles
? `${selectedCount} file${selectedCount === 1 ? '' : 's'} selected`
: 'No files selected'}
</span>
</div>
<CollapsibleContent>
<div className={contentClassName}>
{!hasSelectedFiles ? (
<p className="typography-meta text-muted-foreground">
Select files in Changes to enable commit.
</p>
) : null}
<div className={contentClassName}>
{!hasSelectedFiles ? (
<p className="typography-meta text-muted-foreground">
Select files in Changes to enable commit.
</p>
) : null}
<AIHighlightsBox
highlights={generatedHighlights}
onInsert={onInsertHighlights}
onClear={onClearHighlights}
/>
<AIHighlightsBox
highlights={generatedHighlights}
onInsert={onInsertHighlights}
onClear={onClearHighlights}
/>
<CommitInput
value={commitMessage}
onChange={onCommitMessageChange}
placeholder="Commit message"
disabled={commitAction !== null}
hasTouchInput={hasTouchInput}
isMobile={isMobile}
/>
<CommitInput
value={commitMessage}
onChange={onCommitMessageChange}
placeholder="Commit message"
disabled={commitAction !== null}
hasTouchInput={hasTouchInput}
isMobile={isMobile}
/>
{gitmojiEnabled && (
<Button
variant="outline"
size="sm"
onClick={onOpenGitmojiPicker}
className="w-fit"
type="button"
>
<RiEmotionHappyLine className="size-4" />
Add gitmoji
</Button>
)}
{gitmojiEnabled && (
<Button
variant="outline"
size="sm"
onClick={onOpenGitmojiPicker}
className="w-fit"
type="button"
>
<RiEmotionHappyLine className="size-4" />
Add gitmoji
</Button>
)}
<div className="@container/commit-actions flex items-center gap-2 min-w-0">
<Button
variant="outline"
size="sm"
onClick={onGenerateMessage}
disabled={
isGeneratingMessage ||
commitAction !== null ||
selectedCount === 0 ||
isBusy
}
type="button"
aria-label="Generate"
className="commit-actions__btn"
>
{isGeneratingMessage ? (
<div className="@container/commit-actions flex items-center gap-2 min-w-0">
<Button
variant="outline"
size="sm"
onClick={onGenerateMessage}
disabled={
isGeneratingMessage ||
commitAction !== null ||
selectedCount === 0 ||
isBusy
}
type="button"
aria-label="Generate"
className="commit-actions__btn"
>
{isGeneratingMessage ? (
<RiLoader4Line className="size-4 animate-spin" />
) : (
<RiAiGenerate2 className="size-4 text-primary" />
)}
<span className="commit-actions__label">Generate</span>
</Button>
<div className="flex-1" />
<Button
size="sm"
variant="outline"
onClick={onCommit}
disabled={!canCommit || isGeneratingMessage}
className="commit-actions__btn whitespace-nowrap"
aria-label="Commit"
>
{commitAction === 'commit' ? (
<>
<RiLoader4Line className="size-4 animate-spin" />
) : (
<RiAiGenerate2 className="size-4 text-primary" />
)}
<span className="commit-actions__label">Generate</span>
</Button>
<span className="commit-actions__label">Committing...</span>
</>
) : (
<>
<RiGitCommitLine className="size-4" />
<span className="commit-actions__label">Commit</span>
</>
)}
</Button>
<div className="flex-1" />
<ButtonLarge
variant="outline"
onClick={onCommit}
{isMobile ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
onClick={() => onCommitAndPush()}
disabled={!canCommit || isGeneratingMessage}
className="h-7 w-7 p-0"
aria-label="Push"
>
{commitAction === 'commitAndPush' ? (
<RiLoader4Line className="size-4 animate-spin" />
) : (
<RiArrowUpLine className="size-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Push</p>
</TooltipContent>
</Tooltip>
) : (
<Button
size="sm"
variant="default"
onClick={() => onCommitAndPush()}
disabled={!canCommit || isGeneratingMessage}
className="commit-actions__btn whitespace-nowrap"
aria-label="Commit"
className="commit-actions__btn"
aria-label="Push"
>
{commitAction === 'commit' ? (
{commitAction === 'commitAndPush' ? (
<>
<RiLoader4Line className="size-4 animate-spin" />
<span className="commit-actions__label">Committing...</span>
<span className="commit-actions__label">Pushing...</span>
</>
) : (
<>
<RiGitCommitLine className="size-4" />
<span className="commit-actions__label">Commit</span>
<RiArrowUpLine className="size-3.5" />
<span className="commit-actions__label">Push</span>
</>
)}
</ButtonLarge>
{isMobile ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
onClick={() => onCommitAndPush()}
disabled={!canCommit || isGeneratingMessage}
className="h-7 w-7 p-0"
aria-label="Push"
>
{commitAction === 'commitAndPush' ? (
<RiLoader4Line className="size-4 animate-spin" />
) : (
<RiArrowUpLine className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<p>Push</p>
</TooltipContent>
</Tooltip>
) : (
<ButtonLarge
variant="default"
onClick={() => onCommitAndPush()}
disabled={!canCommit || isGeneratingMessage}
className="commit-actions__btn"
aria-label="Push"
>
{commitAction === 'commitAndPush' ? (
<>
<RiLoader4Line className="size-4 animate-spin" />
<span className="commit-actions__label">Pushing...</span>
</>
) : (
<>
<RiArrowUpLine className="size-4" />
<span className="commit-actions__label">Push</span>
</>
)}
</ButtonLarge>
)}
</div>
</Button>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
</section>
);
};
@@ -261,7 +261,7 @@ Important:
<Button variant="ghost" size="sm" onClick={handleContinueLater} className="flex-1">
Continue Later
</Button>
<Button variant="ghost" size="sm" onClick={handleAbort} className="flex-1 text-[var(--status-error)]">
<Button variant="destructive" size="sm" onClick={handleAbort} className="flex-1">
Abort {operationLabel}
</Button>
</div>
@@ -38,6 +38,8 @@ interface GitHeaderProps {
onFetch: (remote: GitRemote) => void;
onPull: (remote: GitRemote) => void;
onPush: () => void;
onRemoveRemote: (remote: GitRemote) => void;
removingRemoteName: string | null;
onCheckoutBranch: (branch: string) => void;
onCreateBranch: (name: string, remote?: GitRemote) => Promise<void>;
onRenameBranch?: (oldName: string, newName: string) => Promise<void>;
@@ -196,6 +198,8 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
onFetch,
onPull,
onPush,
onRemoveRemote,
removingRemoteName,
onCheckoutBranch,
onCreateBranch,
onRenameBranch,
@@ -241,6 +245,8 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
onFetch={onFetch}
onPull={onPull}
onPush={onPush}
onRemoveRemote={onRemoveRemote}
removingRemoteName={removingRemoteName}
disabled={!status}
iconOnly={true}
tooltipDelayMs={useTwoRowHeader ? 300 : 1000}
@@ -12,8 +12,6 @@ interface HistoryCommitRowProps {
files: CommitFileEntry[];
isLoadingFiles: boolean;
onCopyHash: (hash: string) => void;
roundTop?: boolean;
roundBottom?: boolean;
}
function formatCommitDate(date: string) {
@@ -54,8 +52,6 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
files,
isLoadingFiles,
onCopyHash,
roundTop = false,
roundBottom = false,
}) => {
return (
<li>
@@ -64,8 +60,6 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
onClick={onToggle}
className={cn(
'w-full flex items-start gap-3 px-3 py-2 text-left transition-colors',
roundTop && 'rounded-t-lg',
roundBottom && !isExpanded && 'rounded-b-lg',
isExpanded ? 'bg-sidebar/90' : 'hover:bg-sidebar/40'
)}
>
@@ -113,7 +107,7 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
</button>
{isExpanded && (
<div className={cn('px-3 pb-2 pl-8 border-t border-border/40', roundBottom && 'rounded-b-lg')}>
<div className="px-3 pb-2 pl-8 border-t border-border/40">
{isLoadingFiles ? (
<div className="flex items-center gap-2 py-2">
<RiLoader4Line className="size-4 animate-spin text-muted-foreground" />
@@ -63,6 +63,35 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
branchDivider !== null &&
branchDivider.insertBeforeIndex > 0 &&
branchDivider.insertBeforeIndex < log.all.length;
const hasDividerBelowLoaded = branchDivider !== null && branchDivider.insertBeforeIndex === log.all.length;
const hasSplitHistory = hasDivider || hasDividerBelowLoaded;
const topEntries = hasDivider
? log.all.slice(0, branchDivider.insertBeforeIndex)
: hasDividerBelowLoaded
? log.all
: [];
const bottomEntries = hasDivider ? log.all.slice(branchDivider.insertBeforeIndex) : [];
const dividerIcon = branchDivider?.direction === 'down'
? <RiArrowDownSLine className="size-3.5" />
: <RiArrowUpLine className="size-3.5" />;
const renderCommitList = (entries: GitLogEntry[]) => (
<ul className="divide-y divide-border/60">
{entries.map((entry) => (
<HistoryCommitRow
key={entry.hash}
entry={entry}
isExpanded={expandedCommitHashes.has(entry.hash)}
onToggle={() => onToggleCommit(entry.hash)}
files={commitFilesMap.get(entry.hash) ?? []}
isLoadingFiles={loadingCommitHashes.has(entry.hash)}
onCopyHash={onCopyHash}
/>
))}
</ul>
);
const content = (
<ScrollableOverlay outerClassName="min-h-0 max-h-[50vh]" className="w-full">
@@ -72,50 +101,39 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
No commits found
</p>
</div>
) : (
<ul className="divide-y divide-border/60">
{log.all.map((entry, index) => {
const isBoundary = hasDivider && index === branchDivider.insertBeforeIndex;
const roundTop = isBoundary;
const roundBottom = hasDivider && index === branchDivider.insertBeforeIndex - 1;
) : hasSplitHistory && branchDivider ? (
<div className="flex flex-col gap-0">
{topEntries.length > 0 ? (
<div className="rounded-xl border border-border/60 bg-background/70 overflow-hidden">
{renderCommitList(topEntries)}
</div>
) : null}
return (
<React.Fragment key={entry.hash}>
{isBoundary ? (
<li className="px-3 py-2" aria-hidden>
<div className="flex items-center gap-2">
<span className="h-px flex-1 bg-border/60" />
<span className="inline-flex max-w-[80%] items-center gap-1 typography-micro text-muted-foreground">
<span className="truncate" title={branchDivider.branchName}>{branchDivider.branchName}</span>
{branchDivider.direction === 'down' ? (
<RiArrowDownSLine className="size-3.5" />
) : (
<RiArrowUpLine className="size-3.5" />
)}
</span>
<span className="h-px flex-1 bg-border/60" />
</div>
</li>
) : null}
<HistoryCommitRow
entry={entry}
isExpanded={expandedCommitHashes.has(entry.hash)}
onToggle={() => onToggleCommit(entry.hash)}
files={commitFilesMap.get(entry.hash) ?? []}
isLoadingFiles={loadingCommitHashes.has(entry.hash)}
onCopyHash={onCopyHash}
roundTop={roundTop}
roundBottom={roundBottom}
/>
</React.Fragment>
);
})}
</ul>
<div className="flex items-center gap-2 px-3 py-1.5" aria-hidden>
<span className="h-px flex-1 bg-border/60" />
<span className="inline-flex max-w-[80%] items-center gap-1 typography-micro text-muted-foreground">
<span className="truncate" title={branchDivider.branchName}>{branchDivider.branchName}</span>
{dividerIcon}
</span>
<span className="h-px flex-1 bg-border/60" />
</div>
{bottomEntries.length > 0 ? (
<div className="rounded-xl border border-border/60 bg-background/70 overflow-hidden">
{renderCommitList(bottomEntries)}
</div>
) : null}
</div>
) : (
renderCommitList(log.all)
)}
</ScrollableOverlay>
);
if (!showHeader) {
if (hasSplitHistory) {
return <section>{content}</section>;
}
return (
<section className="rounded-xl border border-border/60 bg-background/70 overflow-hidden">
{content}
@@ -46,7 +46,6 @@ export const IntegrateCommitsSection: React.FC<{
defaultTargetBranch: string;
refreshKey?: number;
onRefresh?: () => void;
variant?: 'framed' | 'plain';
}> = ({
repoRoot,
sourceBranch,
@@ -55,7 +54,6 @@ export const IntegrateCommitsSection: React.FC<{
defaultTargetBranch,
refreshKey,
onRefresh,
variant = 'framed',
}) => {
const currentSessionId = useSessionStore((s) => s.currentSessionId);
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
@@ -337,15 +335,9 @@ Important:
return null;
}
const containerClassName =
variant === 'framed'
? 'rounded-xl border border-border/60 bg-background/70 overflow-hidden'
: 'border-0 bg-transparent rounded-none';
const headerClassName =
variant === 'framed'
? 'px-3 py-2 border-b border-border/40 flex items-center justify-between gap-2'
: 'px-0 py-3 border-b border-border/40 flex items-center justify-between gap-2';
const bodyClassName = variant === 'framed' ? 'flex flex-col gap-3 p-3' : 'flex flex-col gap-3 py-3';
const containerClassName = 'border-0 bg-transparent rounded-none';
const headerClassName = 'px-0 py-3 border-b border-border/40 flex items-center justify-between gap-2';
const bodyClassName = 'flex flex-col gap-3 py-3';
return (
<section className={containerClassName}>
@@ -377,7 +369,7 @@ Important:
<DropdownMenu open={branchDropdownOpen} onOpenChange={setBranchDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-7 px-2 py-0 gap-1.5">
<Button variant="outline" size="sm" className="gap-1.5">
Target
<span className="max-w-[160px] truncate font-mono text-xs text-muted-foreground">{targetBranch}</span>
<RiArrowDownSLine className="size-4 opacity-60" />
@@ -416,15 +408,15 @@ Important:
</DropdownMenu>
{ui.kind === 'ready' ? (
<Button size="sm" className="h-7 px-2 py-0" onClick={() => void handleMove()} disabled={!isEligible || ui.plan.commits.length === 0}>
<Button size="sm" onClick={() => void handleMove()} disabled={!isEligible || ui.plan.commits.length === 0}>
Move
</Button>
) : ui.kind === 'loading' ? (
<Button size="sm" variant="outline" className="h-7 px-2 py-0" disabled>
<Button size="sm" variant="outline" disabled>
Checking
</Button>
) : ui.kind === 'running' ? (
<Button size="sm" variant="outline" className="h-7 px-2 py-0" disabled>
<Button size="sm" variant="outline" disabled>
Moving
</Button>
) : null}
@@ -490,13 +482,13 @@ Important:
)}
</div>
<div className="flex items-center gap-2 pt-1">
<Button size="sm" variant="ghost" className="h-7 px-2 py-0 typography-meta" onClick={() => void handleAbort()}>
<Button size="sm" variant="ghost" className="typography-meta" onClick={() => void handleAbort()}>
Abort
</Button>
<Button
size="sm"
variant="secondary"
className="h-7 px-2 py-0 typography-meta gap-1"
className="typography-meta gap-1"
disabled={!currentSessionId}
onClick={() => void handleResolveWithAi({ state: ui.state, details: ui.details }, false)}
>
@@ -506,13 +498,13 @@ Important:
<Button
size="sm"
variant="secondary"
className="h-7 px-2 py-0 typography-meta gap-1"
className="typography-meta gap-1"
onClick={() => void handleResolveWithAi({ state: ui.state, details: ui.details }, true)}
>
<RiSparklingLine className="size-3.5" />
New Session
</Button>
<Button size="sm" className="h-7 px-2 py-0 typography-meta" onClick={() => void handleContinue()}>
<Button size="sm" className="typography-meta" onClick={() => void handleContinue()}>
Continue
</Button>
</div>
@@ -299,9 +299,8 @@ export const PullRequestSection: React.FC<{
trackingBranch?: string;
remotes?: GitRemote[];
remoteBranches?: string[];
variant?: 'framed' | 'plain';
onGeneratedDescription?: () => void;
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], variant = 'framed', onGeneratedDescription }) => {
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => {
const { github } = useRuntimeAPIs();
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
@@ -1334,15 +1333,9 @@ export const PullRequestSection: React.FC<{
? RiGitClosePullRequestLine
: RiGitPullRequestLine;
const containerClassName =
variant === 'framed'
? 'rounded-xl border border-border/60 bg-transparent overflow-hidden'
: 'border-0 bg-transparent rounded-none';
const headerClassName =
variant === 'framed'
? 'px-3 py-2 border-b border-border/40 flex flex-col gap-1'
: 'px-0 py-3 border-b border-border/40 flex flex-col gap-1';
const bodyClassName = variant === 'framed' ? 'flex flex-col gap-3 p-3' : 'flex flex-col gap-3 py-3';
const containerClassName = 'border-0 bg-transparent rounded-none';
const headerClassName = 'px-0 py-3 border-b border-border/40 flex flex-col gap-1';
const bodyClassName = 'flex flex-col gap-3 py-3';
return (
<section className={containerClassName}>
@@ -1382,7 +1375,7 @@ export const PullRequestSection: React.FC<{
{hasMultipleRemotes ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 px-2 gap-1">
<Button variant="ghost" size="xs" className="gap-1">
<span className="typography-micro">{selectedRemote?.name}</span>
<RiArrowDownSLine className="size-3" />
</Button>
@@ -1693,7 +1686,7 @@ export const PullRequestSection: React.FC<{
</div>
</div>
{repoUrl ? (
<Button variant="outline" size="sm" className="h-7 px-2 py-0" asChild>
<Button variant="outline" size="sm" asChild>
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
<RiExternalLinkLine className="size-4" />
Repo
@@ -1863,7 +1856,6 @@ export const PullRequestSection: React.FC<{
<Button
variant="outline"
size="sm"
className="h-7 px-2 py-0"
onClick={generateDescription}
disabled={isGenerating || isCreating}
>
@@ -1873,7 +1865,7 @@ export const PullRequestSection: React.FC<{
<div className="flex-1" />
<Button
size="sm"
className="h-7 min-w-[7.5rem] justify-center gap-2 px-2 py-0"
className="min-w-[7.5rem] justify-center gap-2"
onClick={createPr}
disabled={isCreating || !isConnected || !targetBaseBranch.trim() || targetBaseBranch.trim() === branch}
>
@@ -3,6 +3,7 @@ import {
RiRefreshLine,
RiArrowDownLine,
RiArrowUpLine,
RiCloseLine,
RiLoader4Line,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
@@ -23,7 +24,9 @@ interface SyncActionsProps {
onFetch: (remote: GitRemote) => void;
onPull: (remote: GitRemote) => void;
onPush: () => void;
onRemoveRemote?: (remote: GitRemote) => void;
disabled: boolean;
removingRemoteName?: string | null;
iconOnly?: boolean;
tooltipDelayMs?: number;
aheadCount?: number;
@@ -36,14 +39,18 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
onFetch,
onPull,
onPush,
onRemoveRemote,
disabled,
removingRemoteName = null,
iconOnly = false,
tooltipDelayMs = 1000,
aheadCount = 0,
behindCount = 0,
}) => {
const skipRemoteSelectRef = React.useRef(false);
const hasNoRemotes = remotes.length === 0;
const isDisabled = disabled || syncAction !== null || hasNoRemotes;
const isRemovingRemote = Boolean(removingRemoteName);
const isDisabled = disabled || syncAction !== null || isRemovingRemote || hasNoRemotes;
const hasMultipleRemotes = remotes.length > 1;
const handleFetch = () => {
@@ -145,14 +152,56 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
</Tooltip>
<DropdownMenuContent align="start" className="min-w-[200px]">
{remotes.map((remote) => (
<DropdownMenuItem key={remote.name} onSelect={() => onSelect(remote)}>
<div className="flex flex-col">
<span className="typography-ui-label text-foreground">
{remote.name}
</span>
<span className="typography-meta text-muted-foreground truncate">
{remote.fetchUrl}
</span>
<DropdownMenuItem
key={remote.name}
onSelect={(event) => {
if (skipRemoteSelectRef.current) {
event.preventDefault();
skipRemoteSelectRef.current = false;
return;
}
onSelect(remote);
}}
>
<div className="flex w-full items-center gap-2">
<div className="min-w-0 flex-1">
<div className="flex flex-col">
<span className="typography-ui-label text-foreground">
{remote.name}
</span>
<span className="typography-meta text-muted-foreground truncate">
{remote.fetchUrl}
</span>
</div>
</div>
{onRemoveRemote ? (
<Button
type="button"
variant="destructive"
size="xs"
className="h-6 w-6 px-0"
disabled={syncAction !== null || isRemovingRemote}
onPointerDown={(event) => {
skipRemoteSelectRef.current = true;
event.preventDefault();
event.stopPropagation();
}}
onClick={(event) => {
skipRemoteSelectRef.current = true;
event.preventDefault();
event.stopPropagation();
onRemoveRemote(remote);
}}
aria-label={`Remove ${remote.name} remote`}
title={`Remove ${remote.name}`}
>
{removingRemoteName === remote.name ? (
<RiLoader4Line className="size-3.5 animate-spin" />
) : (
<RiCloseLine className="size-3.5" />
)}
</Button>
) : null}
</div>
</DropdownMenuItem>
))}