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
@@ -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>
))}