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