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:
committed by
GitHub
co-authored by
Iuliia Ivashko
parent
359879153a
commit
321cc7252a
@@ -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
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user