Add i18n foundation and translations (#1027)
* feat: add i18n foundation * feat: localize sessions sidebar * Localize multirun/scheduled tasks and fix dialog dropdown interactions * localize git sidebar surface and add zh-CN keys * feat(ui): localize context panel, diff/plan views, and context sidebar content * fix(config): resolve user config home via fs/home before embedded home * localize header/chat UI and complete model/worktree panel strings * localize worktree + github issue/pr dialog flows * localize settings sections and split settings i18n dictionaries * localize additional settings sections and sidebars * localize more settings pages and dialogs * fix settings select trigger localization * localize tunnel settings ui surface * localize additional settings sections * localize keyboard shortcuts labels in settings * localize terminal and utility dialogs surfaces * feat(i18n): localize remaining UI strings * Add Ukrainian locale * Add Spanish locale * Add Brazilian Portuguese locale * Polish locale translations
This commit is contained in:
committed by
GitHub
parent
87db2ea210
commit
7d7285655d
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { RiArrowDownLine } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface AIHighlightsBoxProps {
|
||||
highlights: string[];
|
||||
@@ -12,6 +13,7 @@ export const AIHighlightsBox: React.FC<AIHighlightsBoxProps> = ({
|
||||
highlights,
|
||||
onInsert,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
if (highlights.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -23,7 +25,7 @@ export const AIHighlightsBox: React.FC<AIHighlightsBoxProps> = ({
|
||||
return (
|
||||
<div className="space-y-2 rounded-xl border border-border/60 bg-transparent px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="typography-micro text-muted-foreground">AI highlights</p>
|
||||
<p className="typography-micro text-muted-foreground">{t('gitView.commit.aiHighlights.title')}</p>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -31,13 +33,13 @@ export const AIHighlightsBox: React.FC<AIHighlightsBoxProps> = ({
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={handleInsert}
|
||||
aria-label="Insert highlights into commit message"
|
||||
aria-label={t('gitView.commit.aiHighlights.insertAria')}
|
||||
>
|
||||
<RiArrowDownLine className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>
|
||||
Append highlights to commit message
|
||||
{t('gitView.commit.aiHighlights.insertTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from '@/components/ui/command';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type OperationType = 'merge' | 'rebase';
|
||||
|
||||
@@ -66,6 +67,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
onOperationComplete,
|
||||
mode = 'dialog',
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [operation, setOperation] = React.useState<OperationType>('merge');
|
||||
const [selectedBranch, setSelectedBranch] = React.useState<string | null>(null);
|
||||
@@ -75,7 +77,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
const logContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const isDisabled = disabled || isOperating;
|
||||
const targetBranchLabel = currentBranch || 'current branch';
|
||||
const targetBranchLabel = currentBranch || t('gitView.branch.currentBranchFallback');
|
||||
|
||||
// Check if operation completed (all logs are done or error)
|
||||
const operationCompleted = operationLogs.length > 0 &&
|
||||
@@ -199,13 +201,13 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
mode === 'dialog' ? (
|
||||
<DialogFooter>
|
||||
<Button variant="default" size="sm" onClick={handleClose}>
|
||||
{hasError ? 'Close' : 'Done'}
|
||||
{hasError ? t('gitView.common.close') : t('gitView.common.done')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
) : (
|
||||
<div className="flex justify-end">
|
||||
<Button variant="default" size="sm" onClick={handleClose}>
|
||||
{hasError ? 'Close' : 'Done'}
|
||||
{hasError ? t('gitView.common.close') : t('gitView.common.done')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -217,7 +219,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
<div className="space-y-4">
|
||||
{/* Operation Selection */}
|
||||
<div className="space-y-3">
|
||||
<p className="typography-meta text-muted-foreground">Operation</p>
|
||||
<p className="typography-meta text-muted-foreground">{t('gitView.branch.operation')}</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -239,11 +241,11 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
operation === 'merge' ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
Merge
|
||||
{t('gitView.operation.merge')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Combines branches with a merge commit and preserves history.
|
||||
{t('gitView.branch.mergeDescription')}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -267,11 +269,11 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
operation === 'rebase' ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
Rebase
|
||||
{t('gitView.operation.rebase')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Moves your commits to be on top of another branch. Creates linear history.
|
||||
{t('gitView.branch.rebaseDescription')}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
@@ -280,13 +282,15 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
{/* Branch Selection */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{operation === 'merge' ? `Branch to merge into ${targetBranchLabel}` : 'Branch to rebase onto'}
|
||||
{operation === 'merge'
|
||||
? t('gitView.branch.branchToMergeInto', { branch: targetBranchLabel })
|
||||
: t('gitView.branch.branchToRebaseOnto')}
|
||||
</p>
|
||||
<DropdownMenu open={branchDropdownOpen} onOpenChange={setBranchDropdownOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="lg" className="w-full justify-between">
|
||||
<span className={cn('truncate', !selectedBranch && 'text-muted-foreground')}>
|
||||
{selectedBranch || 'Select a branch...'}
|
||||
{selectedBranch || t('gitView.branch.selectBranch')}
|
||||
</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-60 shrink-0" />
|
||||
</Button>
|
||||
@@ -299,15 +303,15 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
<Command className="h-full min-h-0">
|
||||
<CommandInput
|
||||
ref={searchInputRef}
|
||||
placeholder="Search branches..."
|
||||
placeholder={t('gitView.branch.searchPlaceholder')}
|
||||
value={branchSearch}
|
||||
onValueChange={setBranchSearch}
|
||||
/>
|
||||
<CommandList className="h-full min-h-0" disableHorizontal>
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
<CommandEmpty>{t('gitView.branch.empty')}</CommandEmpty>
|
||||
|
||||
{filteredLocal.length > 0 && (
|
||||
<CommandGroup heading="Local branches">
|
||||
<CommandGroup heading={t('gitView.branch.localBranches')}>
|
||||
{filteredLocal.map((branch) => (
|
||||
<CommandItem key={`local-${branch}`} onSelect={() => handleSelectBranch(branch)}>
|
||||
<span className="typography-ui-label text-foreground truncate">{branch}</span>
|
||||
@@ -319,7 +323,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
{filteredLocal.length > 0 && filteredRemote.length > 0 ? <CommandSeparator /> : null}
|
||||
|
||||
{filteredRemote.length > 0 && (
|
||||
<CommandGroup heading="Remote branches">
|
||||
<CommandGroup heading={t('gitView.branch.remoteBranches')}>
|
||||
{filteredRemote.map((branch) => (
|
||||
<CommandItem key={`remote-${branch}`} onSelect={() => handleSelectBranch(branch)}>
|
||||
<span className="typography-ui-label text-foreground truncate">{branch}</span>
|
||||
@@ -339,13 +343,11 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{operation === 'merge' ? (
|
||||
<>
|
||||
This will merge <span className="font-mono text-foreground">{selectedBranch}</span> into{' '}
|
||||
<span className="font-mono text-foreground">{targetBranchLabel}</span>
|
||||
{t('gitView.branch.summaryMergePrefix')} <span className="font-mono text-foreground">{selectedBranch}</span> {t('gitView.branch.summaryMergeInfix')} <span className="font-mono text-foreground">{targetBranchLabel}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This will rebase <span className="font-mono text-foreground">{targetBranchLabel}</span> onto{' '}
|
||||
<span className="font-mono text-foreground">{selectedBranch}</span>
|
||||
{t('gitView.branch.summaryRebasePrefix')} <span className="font-mono text-foreground">{targetBranchLabel}</span> {t('gitView.branch.summaryRebaseInfix')} <span className="font-mono text-foreground">{selectedBranch}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
@@ -355,7 +357,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
{mode === 'dialog' ? (
|
||||
<DialogFooter className="gap-2 pt-1">
|
||||
<Button variant="ghost" size="sm" onClick={handleCancel}>
|
||||
Cancel
|
||||
{t('gitView.common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -367,12 +369,12 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
{operation === 'merge' ? (
|
||||
<>
|
||||
<RiGitMergeLine className="size-4" />
|
||||
Merge
|
||||
{t('gitView.operation.merge')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiGitBranchLine className="size-4" />
|
||||
Rebase
|
||||
{t('gitView.operation.rebase')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -380,11 +382,11 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
) : (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button variant="destructive" size="sm" onClick={handleCancel} disabled={isDisabled}>
|
||||
Reset
|
||||
{t('gitView.common.reset')}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button variant="default" size="sm" onClick={handleConfirm} disabled={isDisabled || !selectedBranch}>
|
||||
{operation === 'merge' ? 'Merge' : 'Rebase'}
|
||||
{operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -398,9 +400,9 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
<section className="border-0 bg-transparent rounded-none">
|
||||
<header className="border-b border-border/40 px-0 py-3">
|
||||
<div className="space-y-1">
|
||||
<div className="typography-ui-header font-semibold text-foreground">Update branch</div>
|
||||
<div className="typography-ui-header font-semibold text-foreground">{t('gitView.branch.updateTitle')}</div>
|
||||
<div className="typography-micro text-muted-foreground">
|
||||
Bring changes from another branch into{' '}
|
||||
{t('gitView.branch.updateDescriptionPrefix')}{' '}
|
||||
<span className="font-mono text-foreground">{targetBranchLabel}</span>.
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,11 +428,11 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
) : (
|
||||
<RiGitMergeLine className="size-4" />
|
||||
)}
|
||||
<span>Merge/Rebase</span>
|
||||
<span>{t('gitView.branch.mergeRebase')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>
|
||||
Merge or rebase changes from another branch.
|
||||
{t('gitView.branch.mergeRebaseTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -443,17 +445,17 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
|
||||
}}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update Branch</DialogTitle>
|
||||
<DialogTitle>{t('gitView.branch.updateTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isOperating ? (
|
||||
operationCompleted ? (
|
||||
hasError ? 'Operation failed' : 'Operation completed'
|
||||
hasError ? t('gitView.branch.operationFailed') : t('gitView.branch.operationCompleted')
|
||||
) : (
|
||||
`${operation === 'merge' ? 'Merging' : 'Rebasing'} in progress...`
|
||||
operation === 'merge' ? t('gitView.branch.mergingInProgress') : t('gitView.branch.rebasingInProgress')
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
Choose how to bring changes from another branch into{' '}
|
||||
{t('gitView.branch.dialogDescriptionPrefix')}{' '}
|
||||
<span className="font-mono text-foreground">{targetBranchLabel}</span>
|
||||
.
|
||||
</>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '@/components/ui/command';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { GitRemote } from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface BranchInfo {
|
||||
ahead?: number;
|
||||
@@ -66,6 +67,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
disabled = false,
|
||||
tooltipDelayMs = 1000,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState('');
|
||||
const [showCreate, setShowCreate] = React.useState(false);
|
||||
@@ -179,21 +181,21 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
>
|
||||
<RiGitBranchLine className="size-4 text-primary" />
|
||||
<span className="min-w-0 truncate font-medium text-left">
|
||||
{currentBranch || 'Detached HEAD'}
|
||||
{currentBranch || t('gitView.branch.detachedHead')}
|
||||
</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-60" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>
|
||||
Current branch
|
||||
{t('gitView.branch.currentBranchTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenuContent align="start" className="w-72 p-0 max-h-[60vh] flex flex-col">
|
||||
<Command className="h-full min-h-0">
|
||||
<CommandInput
|
||||
placeholder="Search branches..."
|
||||
placeholder={t('gitView.branch.searchPlaceholder')}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
@@ -201,7 +203,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
scrollbarClassName="overlay-scrollbar--flush overlay-scrollbar--dense overlay-scrollbar--zero"
|
||||
disableHorizontal
|
||||
>
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
<CommandEmpty>{t('gitView.branch.empty')}</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{showRemoteSelect ? (
|
||||
@@ -217,7 +219,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
<RiArrowLeftLine className="size-4" />
|
||||
</button>
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
Push <span className="text-foreground font-medium">{sanitizedNewBranch}</span> to:
|
||||
{t('gitView.branch.pushToPrefix')} <span className="text-foreground font-medium">{sanitizedNewBranch}</span> {t('gitView.branch.pushToSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -245,13 +247,13 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
) : !showCreate ? (
|
||||
<CommandItem onSelect={handleShowCreate}>
|
||||
<RiAddLine className="size-4" />
|
||||
<span>Create new branch...</span>
|
||||
<span>{t('gitView.branch.create')}</span>
|
||||
</CommandItem>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 rounded-lg">
|
||||
<input
|
||||
ref={createInputRef}
|
||||
placeholder="New branch name"
|
||||
placeholder={t('gitView.branch.newBranchPlaceholder')}
|
||||
value={newBranchName}
|
||||
onChange={(e) => setNewBranchName(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -293,7 +295,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading="Local branches">
|
||||
<CommandGroup heading={t('gitView.branch.localBranches')}>
|
||||
{filteredLocal.map((branch) => (
|
||||
<CommandItem
|
||||
key={`local-${branch}`}
|
||||
@@ -311,14 +313,14 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
)}
|
||||
</span>
|
||||
{currentBranch === branch && (
|
||||
<span className="typography-micro text-primary">Current</span>
|
||||
<span className="typography-micro text-primary">{t('gitView.branch.currentBadge')}</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
{filteredLocal.length === 0 && (
|
||||
<CommandItem disabled className="justify-center">
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
No local branches
|
||||
{t('gitView.branch.noLocalBranches')}
|
||||
</span>
|
||||
</CommandItem>
|
||||
)}
|
||||
@@ -326,7 +328,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading="Remote branches">
|
||||
<CommandGroup heading={t('gitView.branch.remoteBranches')}>
|
||||
{filteredRemote.map((branch) => (
|
||||
<CommandItem
|
||||
key={`remote-${branch}`}
|
||||
@@ -338,7 +340,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
{filteredRemote.length === 0 && (
|
||||
<CommandItem disabled className="justify-center">
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
No remote branches
|
||||
{t('gitView.branch.noRemoteBranches')}
|
||||
</span>
|
||||
</CommandItem>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import type { GitStatus } from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type ChangeDescriptor = {
|
||||
code: string;
|
||||
@@ -64,6 +65,7 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||
indentPx = 0,
|
||||
}) {
|
||||
const descriptor = useMemo(() => describeChange(file), [file]);
|
||||
const { t } = useI18n();
|
||||
const indicatorLabel = descriptor.description;
|
||||
const insertions = stats?.insertions ?? 0;
|
||||
const deletions = stats?.deletions ?? 0;
|
||||
@@ -104,7 +106,7 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||
size="sm"
|
||||
checked={checked}
|
||||
onChange={() => onToggle()}
|
||||
ariaLabel={`Select ${file.path}`}
|
||||
ariaLabel={t('gitView.changes.selectFileAria', { path: file.path })}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
@@ -155,7 +157,7 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||
onClick={handleRevertClick}
|
||||
disabled={isReverting}
|
||||
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label={`Revert changes for ${file.path}`}
|
||||
aria-label={t('gitView.changes.revertFileAria', { path: file.path })}
|
||||
>
|
||||
{isReverting ? (
|
||||
<RiLoader4Line className="size-3.5 animate-spin" />
|
||||
@@ -164,7 +166,7 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>Revert changes</TooltipContent>
|
||||
<TooltipContent sideOffset={8}>{t('gitView.changes.revertFileTooltip')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ChangeRow } from './ChangeRow';
|
||||
import type { GitStatus } from '@/lib/api/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface ChangesSectionProps {
|
||||
changeEntries: GitStatus['files'];
|
||||
@@ -183,6 +184,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
maxListHeightClassName,
|
||||
onVisiblePathsChange,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const scrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const gitChangesViewMode = useUIStore((state) => state.gitChangesViewMode);
|
||||
const isTreeView = gitChangesViewMode === 'tree';
|
||||
@@ -386,7 +388,9 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
type="button"
|
||||
onClick={() => toggleDirectoryExpanded(directory.path)}
|
||||
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
aria-label={isExpanded ? `Collapse ${directory.path}` : `Expand ${directory.path}`}
|
||||
aria-label={isExpanded
|
||||
? t('gitView.changes.collapseDirectoryAria', { path: directory.path })
|
||||
: t('gitView.changes.expandDirectoryAria', { path: directory.path })}
|
||||
>
|
||||
{isExpanded ? <RiArrowDownSLine className="size-4" /> : <RiArrowRightSLine className="size-4" />}
|
||||
</button>
|
||||
@@ -395,7 +399,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={selectionState === 'partial' ? 'mixed' : selectionState === 'all'}
|
||||
aria-label={`Toggle selection for directory ${directory.path}`}
|
||||
aria-label={t('gitView.changes.toggleDirectorySelectionAria', { path: directory.path })}
|
||||
onClick={() => toggleDirectorySelection(directory)}
|
||||
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
@@ -443,7 +447,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
<section className={containerClassName}>
|
||||
<header className={headerClassName}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Changes</h3>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">{t('gitView.changes.title')}</h3>
|
||||
{totalCount > 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -457,7 +461,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
indeterminate={isPartiallySelected}
|
||||
disabled={isRevertingAll}
|
||||
onChange={() => (areAllSelected ? onClearSelection() : onSelectAll())}
|
||||
ariaLabel={areAllSelected ? 'Clear file selection' : 'Select all files'}
|
||||
ariaLabel={areAllSelected ? t('gitView.changes.clearSelectionAria') : t('gitView.changes.selectAllAria')}
|
||||
/>
|
||||
<span className="typography-meta text-muted-foreground">{selectedCount}/{totalCount}</span>
|
||||
</div>
|
||||
@@ -471,7 +475,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
onClick={() => setConfirmRevertAllOpen(true)}
|
||||
disabled={isRevertingAll}
|
||||
>
|
||||
Revert all
|
||||
{t('gitView.changes.revertAll')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -513,7 +517,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div role="list" aria-label="Changed files">
|
||||
<div role="list" aria-label={t('gitView.changes.changedFilesAria')}>
|
||||
{rowItems.map((item, index) => (
|
||||
<div
|
||||
key={isTreeView ? (item as FlattenedTreeRow).key : `file:${(item as GitStatus['files'][number]).path}`}
|
||||
@@ -535,17 +539,19 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
|
||||
<Dialog open={confirmRevertAllOpen} onOpenChange={(open) => { if (!isRevertingAll) setConfirmRevertAllOpen(open); }}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Revert all changes?</DialogTitle>
|
||||
<DialogTitle>{t('gitView.changes.revertAllDialogTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will discard local changes for {totalCount} file{totalCount === 1 ? '' : 's'} in the list.
|
||||
{totalCount === 1
|
||||
? t('gitView.changes.revertAllDescriptionSingle', { count: totalCount })
|
||||
: t('gitView.changes.revertAllDescriptionPlural', { count: totalCount })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" size="sm" onClick={() => setConfirmRevertAllOpen(false)} disabled={isRevertingAll}>
|
||||
Cancel
|
||||
{t('gitView.common.cancel')}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => void handleConfirmRevertAll()} disabled={isRevertingAll}>
|
||||
{isRevertingAll ? 'Reverting...' : 'Revert all'}
|
||||
{isRevertingAll ? t('gitView.changes.reverting') : t('gitView.changes.revertAll')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface CommitInputProps {
|
||||
value: string;
|
||||
@@ -17,11 +18,12 @@ const MAX_HEIGHT = 200;
|
||||
export const CommitInput: React.FC<CommitInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Commit message',
|
||||
placeholder,
|
||||
disabled = false,
|
||||
hasTouchInput = false,
|
||||
isMobile = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||
|
||||
@@ -56,7 +58,7 @@ export const CommitInput: React.FC<CommitInputProps> = ({
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? t('gitView.commit.messagePlaceholder')}
|
||||
rows={1}
|
||||
disabled={disabled}
|
||||
autoCorrect={hasTouchInput ? 'on' : 'off'}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CommitInput } from './CommitInput';
|
||||
import { AIHighlightsBox } from './AIHighlightsBox';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type CommitAction = 'commit' | 'commitAndPush' | null;
|
||||
|
||||
@@ -44,6 +45,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
gitmojiEnabled,
|
||||
onOpenGitmojiPicker,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const hasSelectedFiles = selectedCount > 0;
|
||||
const canCommit = commitMessage.trim() && hasSelectedFiles && commitAction === null;
|
||||
const { isMobile, hasTouchInput } = useDeviceInfo();
|
||||
@@ -55,13 +57,13 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
return (
|
||||
<section className={containerClassName}>
|
||||
<div className={headerClassName}>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Commit</h3>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">{t('gitView.commit.title')}</h3>
|
||||
</div>
|
||||
|
||||
<div className={contentClassName}>
|
||||
{!hasSelectedFiles ? (
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Select files in Changes to enable commit.
|
||||
{t('gitView.commit.selectFilesHint')}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -73,7 +75,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
<CommitInput
|
||||
value={commitMessage}
|
||||
onChange={onCommitMessageChange}
|
||||
placeholder="Commit message"
|
||||
placeholder={t('gitView.commit.messagePlaceholder')}
|
||||
disabled={commitAction !== null}
|
||||
hasTouchInput={hasTouchInput}
|
||||
isMobile={isMobile}
|
||||
@@ -88,7 +90,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
type="button"
|
||||
>
|
||||
<RiEmotionHappyLine className="size-4" />
|
||||
Add gitmoji
|
||||
{t('gitView.commit.addGitmoji')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -104,7 +106,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
isBusy
|
||||
}
|
||||
type="button"
|
||||
aria-label="Generate"
|
||||
aria-label={t('gitView.commit.generateAria')}
|
||||
className="commit-actions__btn"
|
||||
>
|
||||
{isGeneratingMessage ? (
|
||||
@@ -112,7 +114,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
) : (
|
||||
<RiAiGenerate2 className="size-4 text-primary" />
|
||||
)}
|
||||
<span className="commit-actions__label">Generate</span>
|
||||
<span className="commit-actions__label">{t('gitView.commit.generate')}</span>
|
||||
</Button>
|
||||
|
||||
<div className="flex-1" />
|
||||
@@ -123,17 +125,17 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
onClick={onCommit}
|
||||
disabled={!canCommit || isGeneratingMessage}
|
||||
className="commit-actions__btn whitespace-nowrap"
|
||||
aria-label="Commit"
|
||||
aria-label={t('gitView.commit.commitAria')}
|
||||
>
|
||||
{commitAction === 'commit' ? (
|
||||
<>
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
<span className="commit-actions__label">Committing...</span>
|
||||
<span className="commit-actions__label">{t('gitView.commit.committing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiGitCommitLine className="size-4" />
|
||||
<span className="commit-actions__label">Commit</span>
|
||||
<span className="commit-actions__label">{t('gitView.commit.commit')}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -147,7 +149,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
onClick={() => onCommitAndPush()}
|
||||
disabled={!canCommit || isGeneratingMessage}
|
||||
className="h-7 w-7 p-0"
|
||||
aria-label="Push"
|
||||
aria-label={t('gitView.commit.pushAria')}
|
||||
>
|
||||
{commitAction === 'commitAndPush' ? (
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
@@ -157,7 +159,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Push</p>
|
||||
<p>{t('gitView.commit.push')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -167,17 +169,17 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
onClick={() => onCommitAndPush()}
|
||||
disabled={!canCommit || isGeneratingMessage}
|
||||
className="commit-actions__btn"
|
||||
aria-label="Push"
|
||||
aria-label={t('gitView.commit.pushAria')}
|
||||
>
|
||||
{commitAction === 'commitAndPush' ? (
|
||||
<>
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
<span className="commit-actions__label">Pushing...</span>
|
||||
<span className="commit-actions__label">{t('gitView.commit.pushing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiArrowUpLine className="size-3.5" />
|
||||
<span className="commit-actions__label">Push</span>
|
||||
<span className="commit-actions__label">{t('gitView.commit.push')}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import { toast } from '@/components/ui';
|
||||
import { getConflictDetails, type MergeConflictDetails } from '@/lib/gitApi';
|
||||
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface ConflictDialogProps {
|
||||
open: boolean;
|
||||
@@ -35,6 +36,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
onAbort,
|
||||
onClearState,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
const setPendingInputText = useInputStore((state) => state.setPendingInputText);
|
||||
@@ -58,7 +60,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
setConflictDetails(details);
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load conflict details';
|
||||
const message = err instanceof Error ? err.message : t('gitView.conflict.loadFailed');
|
||||
setLoadError(message);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -119,12 +121,12 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
const handleResolveInCurrentSession = async () => {
|
||||
const context = await buildConflictContext();
|
||||
if (!context) {
|
||||
toast.error('No conflict details available');
|
||||
toast.error(t('gitView.conflict.noDetailsAvailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSessionId) {
|
||||
toast.error('No active session', { description: 'Open a chat session first or use "New Session".' });
|
||||
toast.error(t('gitView.conflict.noActiveSession'), { description: t('gitView.conflict.noActiveSessionDescription') });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,7 +145,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
const handleResolveInNewSession = async () => {
|
||||
const context = await buildConflictContext();
|
||||
if (!context) {
|
||||
toast.error('No conflict details available');
|
||||
toast.error(t('gitView.conflict.noDetailsAvailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,7 +164,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const operationLabel = operation === 'merge' ? 'Merge' : 'Rebase';
|
||||
const operationLabel = operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase');
|
||||
const displayFiles = conflictDetails?.unmergedFiles || conflictFiles;
|
||||
|
||||
return (
|
||||
@@ -172,30 +174,30 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<RiAlertLine className="size-5 shrink-0 text-[var(--status-warning)]" />
|
||||
<DialogTitle>{operationLabel} Conflicts Detected</DialogTitle>
|
||||
<DialogTitle>{t('gitView.conflict.detectedTitle', { operation: operationLabel })}</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription>
|
||||
The {operation} operation resulted in conflicts that need to be resolved.
|
||||
{t('gitView.conflict.detectedDescription', { operation })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center gap-2 py-4 text-muted-foreground">
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
<span className="typography-meta">Loading conflict details...</span>
|
||||
<span className="typography-meta">{t('gitView.conflict.loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadError && (
|
||||
<div className="rounded-lg bg-[var(--status-error-bg)] p-3 text-[var(--status-error)] typography-meta break-words">
|
||||
Error loading details: {loadError}
|
||||
{t('gitView.conflict.errorLoadingDetails', { message: loadError })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayFiles.length > 0 && (
|
||||
<div className="space-y-2 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="typography-meta text-muted-foreground">Conflicted files:</p>
|
||||
<p className="typography-meta text-muted-foreground">{t('gitView.conflict.conflictedFiles')}</p>
|
||||
<span className="typography-micro px-1.5 py-0.5 rounded bg-[var(--surface-elevated)] text-muted-foreground">
|
||||
{displayFiles.length}
|
||||
</span>
|
||||
@@ -218,7 +220,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
|
||||
{conflictDetails?.headInfo && (
|
||||
<div className="space-y-1 overflow-hidden">
|
||||
<p className="typography-meta text-muted-foreground">HEAD information:</p>
|
||||
<p className="typography-meta text-muted-foreground">{t('gitView.conflict.headInfo')}</p>
|
||||
<div className="typography-micro text-foreground font-mono bg-[var(--surface-elevated)] rounded-lg p-3 max-h-24 overflow-y-auto break-words whitespace-pre-wrap">
|
||||
{conflictDetails.headInfo}
|
||||
</div>
|
||||
@@ -239,7 +241,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
) : (
|
||||
<RiAddLine className="size-4" />
|
||||
)}
|
||||
Resolve in New Session
|
||||
{t('gitView.conflict.resolveNewSession')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -252,14 +254,14 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
) : (
|
||||
<RiChat1Line className="size-4" />
|
||||
)}
|
||||
Resolve in Current Session
|
||||
{t('gitView.conflict.resolveCurrentSession')}
|
||||
</Button>
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button variant="ghost" size="sm" onClick={handleContinueLater} className="flex-1">
|
||||
Continue Later
|
||||
{t('gitView.conflict.continueLater')}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={handleAbort} className="flex-1">
|
||||
Abort {operationLabel}
|
||||
{t('gitView.conflict.abortOperation', { operation: operationLabel })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RiGitCommitLine, RiArrowDownLine, RiLoader4Line } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface GitEmptyStateProps {
|
||||
behind: number;
|
||||
@@ -13,14 +14,15 @@ export const GitEmptyState: React.FC<GitEmptyStateProps> = ({
|
||||
onPull,
|
||||
isPulling,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
||||
<RiGitCommitLine className="size-10 text-muted-foreground/70 mb-4" />
|
||||
<p className="typography-ui-label font-semibold text-foreground mb-1">
|
||||
Working tree clean
|
||||
{t('gitView.empty.cleanTitle')}
|
||||
</p>
|
||||
<p className="typography-meta text-muted-foreground mb-4">
|
||||
All changes have been committed
|
||||
{t('gitView.empty.cleanDescription')}
|
||||
</p>
|
||||
|
||||
{behind > 0 && (
|
||||
@@ -34,7 +36,9 @@ export const GitEmptyState: React.FC<GitEmptyStateProps> = ({
|
||||
) : (
|
||||
<RiArrowDownLine className="size-4" />
|
||||
)}
|
||||
Pull {behind} commit{behind === 1 ? '' : 's'}
|
||||
{behind === 1
|
||||
? t('gitView.empty.pullBehindSingle', { count: behind })
|
||||
: t('gitView.empty.pullBehindPlural', { count: behind })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { WorktreeBranchDisplay } from './WorktreeBranchDisplay';
|
||||
import { SyncActions } from './SyncActions';
|
||||
import type { GitStatus, GitIdentityProfile, GitRemote } from '@/lib/api/types';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type SyncAction = 'fetch' | 'pull' | 'push' | null;
|
||||
|
||||
@@ -115,6 +116,7 @@ const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
|
||||
tooltipDelayMs = 1000,
|
||||
iconOnly = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const isDisabled = isApplying || identities.length === 0;
|
||||
|
||||
return (
|
||||
@@ -140,20 +142,20 @@ const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
|
||||
)}
|
||||
{!iconOnly && (
|
||||
<span className="git-identity-label min-w-0 flex-1 truncate text-left">
|
||||
{activeProfile?.name || 'No identity'}
|
||||
{activeProfile?.name || t('gitView.header.noIdentity')}
|
||||
</span>
|
||||
)}
|
||||
<RiArrowDownSLine className="size-4 opacity-60" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>Git identity</TooltipContent>
|
||||
<TooltipContent sideOffset={8}>{t('gitView.header.identityTooltip')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
{identities.length === 0 ? (
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
No profiles available to apply.
|
||||
{t('gitView.header.noProfiles')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -210,6 +212,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
isWorktreeMode,
|
||||
onOpenHistory,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
|
||||
if (!status) {
|
||||
@@ -232,7 +235,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
<RiHistoryLine className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>History</TooltipContent>
|
||||
<TooltipContent sideOffset={8}>{t('gitView.history.title')}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { GitLogEntry, CommitFileEntry } from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface HistoryCommitRowProps {
|
||||
entry: GitLogEntry;
|
||||
@@ -53,6 +54,7 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
||||
isLoadingFiles,
|
||||
onCopyHash,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
@@ -100,7 +102,7 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
||||
<RiFileCopyLine className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>Copy SHA</TooltipContent>
|
||||
<TooltipContent sideOffset={8}>{t('gitView.history.copySha')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,10 +113,10 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
||||
{isLoadingFiles ? (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<RiLoader4Line className="size-4 animate-spin text-muted-foreground" />
|
||||
<span className="typography-micro text-muted-foreground">Loading files...</span>
|
||||
<span className="typography-micro text-muted-foreground">{t('gitView.history.loadingFiles')}</span>
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<p className="typography-micro text-muted-foreground py-2">No files</p>
|
||||
<p className="typography-micro text-muted-foreground py-2">{t('gitView.history.noFiles')}</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5 py-2">
|
||||
{files.map((file) => (
|
||||
@@ -146,7 +148,7 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
||||
)}
|
||||
{file.isBinary && (
|
||||
<span className="typography-micro text-muted-foreground shrink-0">
|
||||
binary
|
||||
{t('gitView.history.binary')}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { HistoryCommitRow } from './HistoryCommitRow';
|
||||
import type { GitLogEntry, CommitFileEntry } from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
const LOG_SIZE_OPTIONS = [
|
||||
{ label: '25 commits', value: 25 },
|
||||
{ label: '50 commits', value: 50 },
|
||||
{ label: '100 commits', value: 100 },
|
||||
{ labelKey: 'gitView.history.logSize25', value: 25 },
|
||||
{ labelKey: 'gitView.history.logSize50', value: 50 },
|
||||
{ labelKey: 'gitView.history.logSize100', value: 100 },
|
||||
];
|
||||
|
||||
interface HistorySectionProps {
|
||||
@@ -53,6 +54,7 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
||||
showHeader = true,
|
||||
branchDivider = null,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
|
||||
if (!log) {
|
||||
@@ -98,7 +100,7 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
||||
{log.all.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center p-4">
|
||||
<p className="typography-ui-label text-muted-foreground">
|
||||
No commits found
|
||||
{t('gitView.history.noCommits')}
|
||||
</p>
|
||||
</div>
|
||||
) : hasSplitHistory && branchDivider ? (
|
||||
@@ -148,7 +150,7 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
||||
className="rounded-xl border border-border/60 bg-background/70 overflow-hidden"
|
||||
>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 h-10 hover:bg-transparent">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">History</h3>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">{t('gitView.history.title')}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{isOpen && (
|
||||
<div
|
||||
@@ -165,12 +167,12 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
||||
className="data-[size=sm]:h-auto h-7 min-h-7 w-auto justify-between px-2 py-0"
|
||||
disabled={isLogLoading}
|
||||
>
|
||||
<SelectValue placeholder="Commits" />
|
||||
<SelectValue placeholder={t('gitView.history.commitsPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOG_SIZE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={String(option.value)}>
|
||||
{option.label}
|
||||
{t(option.labelKey as never)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { GitMergeInProgress, GitRebaseInProgress } from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface InProgressOperationBannerProps {
|
||||
mergeInProgress: GitMergeInProgress | null | undefined;
|
||||
@@ -29,6 +30,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
hasUnresolvedConflicts = false,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [processingAction, setProcessingAction] = React.useState<'continue' | 'abort' | null>(null);
|
||||
|
||||
// Only show banner if we have actual in-progress operation data
|
||||
@@ -60,19 +62,19 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
|
||||
const isProcessing = processingAction !== null;
|
||||
|
||||
const operationLabel = operation === 'merge' ? 'Merge' : 'Rebase';
|
||||
const operationLabel = operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase');
|
||||
const OperationIcon = operation === 'merge' ? RiGitMergeLine : RiGitBranchLine;
|
||||
|
||||
// Build description
|
||||
let description = '';
|
||||
if (mergeInProgress) {
|
||||
description = mergeInProgress.message
|
||||
? `Merging: ${mergeInProgress.message}`
|
||||
: `Merge in progress (${mergeInProgress.head})`;
|
||||
? t('gitView.operation.mergingMessage', { message: mergeInProgress.message })
|
||||
: t('gitView.operation.mergeInProgressWithHead', { head: mergeInProgress.head });
|
||||
} else if (rebaseInProgress) {
|
||||
description = rebaseInProgress.headName
|
||||
? `Rebasing ${rebaseInProgress.headName} onto ${rebaseInProgress.onto}`
|
||||
: `Rebase in progress`;
|
||||
? t('gitView.operation.rebasingOnto', { headName: rebaseInProgress.headName, onto: rebaseInProgress.onto || '' })
|
||||
: t('gitView.operation.rebaseInProgress');
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -82,7 +84,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
<OperationIcon className="size-4 text-[var(--status-warning)] shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="typography-label text-[var(--status-warning)]">
|
||||
{operationLabel} in Progress
|
||||
{t('gitView.operation.inProgressTitle', { operation: operationLabel })}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="typography-micro text-muted-foreground truncate">
|
||||
@@ -102,7 +104,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
className="gap-1.5"
|
||||
>
|
||||
<RiSparklingLine className="size-4" />
|
||||
Resolve with AI
|
||||
{t('gitView.operation.resolveWithAi')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -119,7 +121,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
) : (
|
||||
<RiCloseLine className="size-4" />
|
||||
)}
|
||||
Abort
|
||||
{t('gitView.operation.abort')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -136,7 +138,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
) : (
|
||||
<RiCheckLine className="size-4" />
|
||||
)}
|
||||
Continue
|
||||
{t('gitView.operation.continue')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -144,7 +146,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
|
||||
|
||||
{hasUnresolvedConflicts && (
|
||||
<p className="typography-micro text-[var(--status-warning)] mt-2">
|
||||
Conflicts must be resolved before continuing. Use "Resolve with AI" or resolve manually, then stage changes and click Continue.
|
||||
{t('gitView.operation.resolveConflictsHint')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type IntegratePlan,
|
||||
} from '@/lib/git/integrateWorktreeCommits';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type IntegrateUiState =
|
||||
| { kind: 'idle' }
|
||||
@@ -57,6 +58,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
refreshKey,
|
||||
onRefresh,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
|
||||
const [branchDropdownOpen, setBranchDropdownOpen] = React.useState(false);
|
||||
@@ -240,7 +242,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
|
||||
// Use current session - set pending input text and synthetic parts
|
||||
if (!currentSessionId) {
|
||||
toast.error('No active session', { description: 'Open a chat session first or start a new session.' });
|
||||
toast.error(t('gitView.integrate.noActiveSession'), { description: t('gitView.integrate.noActiveSessionDescription') });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -255,15 +257,17 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
const handleMove = React.useCallback(async () => {
|
||||
if (ui.kind !== 'ready') return;
|
||||
if (ui.plan.commits.length === 0) {
|
||||
toast.message('No commits to move');
|
||||
toast.message(t('gitView.integrate.noCommitsToMoveToast'));
|
||||
return;
|
||||
}
|
||||
setUi({ kind: 'running', plan: ui.plan });
|
||||
try {
|
||||
const result = await integrateWorktreeCommits(ui.plan);
|
||||
if (result.kind === 'success') {
|
||||
toast.success('Commits moved', {
|
||||
description: `${result.moved} commit${result.moved === 1 ? '' : 's'} into ${ui.plan.targetBranch}`,
|
||||
toast.success(t('gitView.integrate.commitsMovedToast'), {
|
||||
description: result.moved === 1
|
||||
? t('gitView.integrate.commitsMovedDescriptionSingle', { count: result.moved, branch: ui.plan.targetBranch })
|
||||
: t('gitView.integrate.commitsMovedDescriptionPlural', { count: result.moved, branch: ui.plan.targetBranch }),
|
||||
});
|
||||
const next = await computeIntegratePlan(ui.plan);
|
||||
setUi({ kind: 'ready', plan: next });
|
||||
@@ -271,7 +275,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
return;
|
||||
}
|
||||
if (result.kind === 'conflict') {
|
||||
toast.error('Cherry-pick conflict', { description: 'Resolve conflicts, then continue.' });
|
||||
toast.error(t('gitView.integrate.cherryPickConflictToast'), { description: t('gitView.integrate.cherryPickConflictDescription') });
|
||||
setUi({ kind: 'conflict', state: result.state, details: result.details });
|
||||
if (conflictStorageKey && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(conflictStorageKey, JSON.stringify(result.state));
|
||||
@@ -279,7 +283,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to move commits', { description: message });
|
||||
toast.error(t('gitView.integrate.failedToMoveToast'), { description: message });
|
||||
const next = await computeIntegratePlan({ repoRoot, sourceBranch, targetBranch }).catch(() => null);
|
||||
if (next) setUi({ kind: 'ready', plan: next });
|
||||
else setUi({ kind: 'idle' });
|
||||
@@ -290,7 +294,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
if (ui.kind !== 'conflict') return;
|
||||
try {
|
||||
await abortIntegrate(ui.state);
|
||||
toast.message('Cherry-pick aborted');
|
||||
toast.message(t('gitView.integrate.cherryPickAbortedToast'));
|
||||
if (conflictStorageKey && typeof window !== 'undefined') {
|
||||
window.localStorage.removeItem(conflictStorageKey);
|
||||
}
|
||||
@@ -306,7 +310,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
try {
|
||||
const result = await continueIntegrate(ui.state);
|
||||
if (result.kind === 'success') {
|
||||
toast.success('Cherry-pick finished');
|
||||
toast.success(t('gitView.integrate.cherryPickFinishedToast'));
|
||||
const next = await computeIntegratePlan({ repoRoot, sourceBranch, targetBranch }).catch(() => null);
|
||||
if (next) setUi({ kind: 'ready', plan: next });
|
||||
else setUi({ kind: 'idle' });
|
||||
@@ -324,7 +328,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Cherry-pick continue failed', { description: message });
|
||||
toast.error(t('gitView.integrate.cherryPickContinueFailedToast'), { description: message });
|
||||
}
|
||||
}, [ui, repoRoot, sourceBranch, targetBranch, onRefresh, conflictStorageKey]);
|
||||
|
||||
@@ -341,9 +345,11 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
<div className={headerClassName}>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<RiSplitCellsHorizontal className="size-4 text-muted-foreground" />
|
||||
<h3 className="typography-ui-header font-semibold text-foreground truncate">Re-integrate commits</h3>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground truncate">{t('gitView.integrate.title')}</h3>
|
||||
{ui.kind === 'ready' && ui.plan.commits.length > 0 ? (
|
||||
<span className="typography-meta text-muted-foreground truncate">{ui.plan.commits.length} to move</span>
|
||||
<span className="typography-meta text-muted-foreground truncate">
|
||||
{t('gitView.integrate.toMoveCount', { count: ui.plan.commits.length })}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -356,7 +362,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
<div className={bodyClassName}>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="typography-ui-label text-foreground">Move commits</div>
|
||||
<div className="typography-ui-label text-foreground">{t('gitView.integrate.moveCommits')}</div>
|
||||
<div className="typography-micro text-muted-foreground truncate">
|
||||
{sourceBranch} → {targetBranch}
|
||||
</div>
|
||||
@@ -367,7 +373,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
<DropdownMenu open={branchDropdownOpen} onOpenChange={setBranchDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-1.5">
|
||||
Target
|
||||
{t('gitView.integrate.target')}
|
||||
<span className="max-w-[160px] truncate font-mono text-xs text-muted-foreground">{targetBranch}</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-60" />
|
||||
</Button>
|
||||
@@ -377,14 +383,14 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
className="w-72 p-0 max-h-[var(--available-height)] flex flex-col overflow-hidden"
|
||||
>
|
||||
<Command className="h-full min-h-0">
|
||||
<CommandInput ref={searchInputRef} placeholder="Search branches..." />
|
||||
<CommandInput ref={searchInputRef} placeholder={t('gitView.branch.searchPlaceholder')} />
|
||||
<CommandList
|
||||
className="h-full min-h-0"
|
||||
scrollbarClassName="overlay-scrollbar--flush overlay-scrollbar--dense overlay-scrollbar--zero"
|
||||
disableHorizontal
|
||||
>
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
<CommandGroup heading="Local branches">
|
||||
<CommandEmpty>{t('gitView.branch.empty')}</CommandEmpty>
|
||||
<CommandGroup heading={t('gitView.branch.localBranches')}>
|
||||
{localBranches.map((branch) => (
|
||||
<CommandItem
|
||||
key={branch}
|
||||
@@ -406,28 +412,28 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
|
||||
{ui.kind === 'ready' ? (
|
||||
<Button size="sm" onClick={() => void handleMove()} disabled={!isEligible || ui.plan.commits.length === 0}>
|
||||
Move
|
||||
{t('gitView.integrate.move')}
|
||||
</Button>
|
||||
) : ui.kind === 'loading' ? (
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
Checking…
|
||||
{t('gitView.integrate.checking')}
|
||||
</Button>
|
||||
) : ui.kind === 'running' ? (
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
Moving…
|
||||
{t('gitView.integrate.moving')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{ui.kind === 'ready' && ui.plan.commits.length === 0 && (
|
||||
<div className="typography-meta text-muted-foreground">No commits to move.</div>
|
||||
<div className="typography-meta text-muted-foreground">{t('gitView.integrate.noCommitsToMove')}</div>
|
||||
)}
|
||||
|
||||
{ui.kind === 'ready' && ui.plan.commits.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="typography-meta text-foreground">
|
||||
Commits to move
|
||||
{t('gitView.integrate.commitsToMove')}
|
||||
<span className="text-muted-foreground"> ({ui.plan.commits.length})</span>
|
||||
</div>
|
||||
{commitSummaries.length > 0 && ui.plan.commits.length > 5 && (
|
||||
@@ -436,7 +442,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
onClick={() => setShowAllCommits((v) => !v)}
|
||||
className="typography-micro text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showAllCommits ? 'Show less' : 'Show all'}
|
||||
{showAllCommits ? t('gitView.integrate.showLess') : t('gitView.integrate.showAll')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -449,11 +455,11 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
</div>
|
||||
))}
|
||||
{commitSummaries.length === 0 && (
|
||||
<div className="typography-meta text-muted-foreground">Preview unavailable.</div>
|
||||
<div className="typography-meta text-muted-foreground">{t('gitView.integrate.previewUnavailable')}</div>
|
||||
)}
|
||||
{ui.plan.commits.length > commitSummaries.length && (
|
||||
<div className="typography-micro text-muted-foreground/70">
|
||||
Showing first {commitSummaries.length} commits.
|
||||
{t('gitView.integrate.showingFirstCommits', { count: commitSummaries.length })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -463,10 +469,10 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
{ui.kind === 'conflict' && (
|
||||
<div className="rounded-md border border-border/60 bg-background/60 p-3 space-y-2">
|
||||
<div className="typography-meta text-foreground">
|
||||
Conflicts in {ui.details.unmergedFiles.length} files
|
||||
{t('gitView.integrate.conflictsInFiles', { count: ui.details.unmergedFiles.length })}
|
||||
</div>
|
||||
<div className="typography-micro text-muted-foreground/80">
|
||||
Current commit: <span className="font-mono">{ui.state.currentCommit.slice(0, 7)}</span>
|
||||
{t('gitView.integrate.currentCommit')}: <span className="font-mono">{ui.state.currentCommit.slice(0, 7)}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{ui.details.unmergedFiles.slice(0, 6).map((file) => (
|
||||
@@ -475,12 +481,12 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
</span>
|
||||
))}
|
||||
{ui.details.unmergedFiles.length > 6 && (
|
||||
<span className="text-xs text-muted-foreground">+{ui.details.unmergedFiles.length - 6} more</span>
|
||||
<span className="text-xs text-muted-foreground">{t('gitView.integrate.moreFiles', { count: ui.details.unmergedFiles.length - 6 })}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button size="sm" variant="ghost" className="typography-meta" onClick={() => void handleAbort()}>
|
||||
Abort
|
||||
{t('gitView.operation.abort')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -490,7 +496,7 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
onClick={() => void handleResolveWithAi({ state: ui.state, details: ui.details }, false)}
|
||||
>
|
||||
<RiSparklingLine className="size-3.5" />
|
||||
Current Session
|
||||
{t('gitView.integrate.currentSession')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -499,10 +505,10 @@ export const IntegrateCommitsSection: React.FC<{
|
||||
onClick={() => void handleResolveWithAi({ state: ui.state, details: ui.details }, true)}
|
||||
>
|
||||
<RiSparklingLine className="size-3.5" />
|
||||
New Session
|
||||
{t('gitView.integrate.newSession')}
|
||||
</Button>
|
||||
<Button size="sm" className="typography-meta" onClick={() => void handleContinue()}>
|
||||
Continue
|
||||
{t('gitView.operation.continue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,7 @@ import type {
|
||||
GitHubPullRequestStatus,
|
||||
GitRemote,
|
||||
} from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type MergeMethod = 'merge' | 'squash' | 'rebase';
|
||||
|
||||
@@ -278,6 +279,7 @@ export const PullRequestSection: React.FC<{
|
||||
remoteBranches?: string[];
|
||||
onGeneratedDescription?: () => void;
|
||||
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => {
|
||||
const { t } = useI18n();
|
||||
const { github } = useRuntimeAPIs();
|
||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||
@@ -516,7 +518,7 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
const openChecksDialog = React.useCallback(async () => {
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
if (!pr) return;
|
||||
@@ -532,7 +534,7 @@ export const PullRequestSection: React.FC<{
|
||||
setCheckDetails(ctx);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load check details', { description: message });
|
||||
toast.error(t('gitView.pr.toast.loadCheckDetailsFailed'), { description: message });
|
||||
} finally {
|
||||
setIsLoadingCheckDetails(false);
|
||||
}
|
||||
@@ -540,7 +542,7 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
const openCommentsDialog = React.useCallback(async () => {
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
if (!pr) return;
|
||||
@@ -555,7 +557,7 @@ export const PullRequestSection: React.FC<{
|
||||
setCommentsDetails(ctx);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load comments', { description: message });
|
||||
toast.error(t('gitView.pr.toast.loadCommentsFailed'), { description: message });
|
||||
} finally {
|
||||
setIsLoadingCommentsDetails(false);
|
||||
}
|
||||
@@ -594,11 +596,11 @@ export const PullRequestSection: React.FC<{
|
||||
const issue = (commentsDetails?.issueComments ?? []).map((comment) => ({
|
||||
id: `issue-${comment.id}`,
|
||||
body: comment.body || '',
|
||||
authorName: comment.author?.name || comment.author?.login || 'Unknown author',
|
||||
authorName: comment.author?.name || comment.author?.login || t('gitView.pr.comments.unknownAuthor'),
|
||||
authorLogin: comment.author?.login || null,
|
||||
avatarUrl: comment.author?.avatarUrl || null,
|
||||
createdAt: comment.createdAt,
|
||||
context: 'General comment',
|
||||
context: t('gitView.pr.comments.generalContext'),
|
||||
path: null as string | null,
|
||||
line: null as number | null,
|
||||
}));
|
||||
@@ -606,11 +608,11 @@ export const PullRequestSection: React.FC<{
|
||||
const review = (commentsDetails?.reviewComments ?? []).map((comment) => ({
|
||||
id: `review-${comment.id}`,
|
||||
body: comment.body || '',
|
||||
authorName: comment.author?.name || comment.author?.login || 'Unknown author',
|
||||
authorName: comment.author?.name || comment.author?.login || t('gitView.pr.comments.unknownAuthor'),
|
||||
authorLogin: comment.author?.login || null,
|
||||
avatarUrl: comment.author?.avatarUrl || null,
|
||||
createdAt: comment.createdAt,
|
||||
context: 'Code review comment',
|
||||
context: t('gitView.pr.comments.reviewContext'),
|
||||
path: comment.path || null,
|
||||
line: comment.line ?? null,
|
||||
}));
|
||||
@@ -624,11 +626,11 @@ export const PullRequestSection: React.FC<{
|
||||
return aVal - bVal;
|
||||
});
|
||||
return all;
|
||||
}, [commentsDetails]);
|
||||
}, [commentsDetails, t]);
|
||||
|
||||
const resolveChatDispatchTarget = React.useCallback((): ChatDispatchTarget | null => {
|
||||
if (!currentSessionId) {
|
||||
toast.error('No active session', { description: 'Open a chat session first.' });
|
||||
toast.error(t('gitView.pr.toast.noActiveSession'), { description: t('gitView.pr.toast.noActiveSessionDescription') });
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -637,7 +639,7 @@ export const PullRequestSection: React.FC<{
|
||||
const providerID = currentProviderId || lastUsedProvider?.providerID;
|
||||
const modelID = currentModelId || lastUsedProvider?.modelID;
|
||||
if (!providerID || !modelID) {
|
||||
toast.error('No model selected');
|
||||
toast.error(t('gitView.pr.toast.noModelSelected'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -670,7 +672,7 @@ export const PullRequestSection: React.FC<{
|
||||
target.currentVariant ?? undefined,
|
||||
).catch((e) => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to send message', { description: message });
|
||||
toast.error(t('gitView.pr.toast.sendMessageFailed'), { description: message });
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -743,7 +745,7 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
{run.job?.steps && run.job.steps.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
<div className="typography-micro text-muted-foreground">Steps</div>
|
||||
<div className="typography-micro text-muted-foreground">{t('gitView.pr.checks.steps')}</div>
|
||||
<div className="space-y-1">
|
||||
{run.job.steps.map((step, idx) => {
|
||||
const c = (step.conclusion || '').toLowerCase();
|
||||
@@ -787,11 +789,11 @@ export const PullRequestSection: React.FC<{
|
||||
</button>
|
||||
<CollapsibleContent>
|
||||
<div className="ml-6 mt-1 rounded border border-border/40 bg-transparent px-2 py-2 typography-micro text-muted-foreground space-y-1">
|
||||
{typeof step.number === 'number' ? <div>Step: {step.number}</div> : null}
|
||||
{step.status ? <div>Status: {step.status}</div> : null}
|
||||
{step.conclusion ? <div>Conclusion: {step.conclusion}</div> : null}
|
||||
{step.startedAt ? <div>Started: {formatTimestamp(step.startedAt)}</div> : null}
|
||||
{step.completedAt ? <div>Completed: {formatTimestamp(step.completedAt)}</div> : null}
|
||||
{typeof step.number === 'number' ? <div>{t('gitView.pr.checks.stepLabel')}: {step.number}</div> : null}
|
||||
{step.status ? <div>{t('gitView.pr.checks.statusLabel')}: {step.status}</div> : null}
|
||||
{step.conclusion ? <div>{t('gitView.pr.checks.conclusionLabel')}: {step.conclusion}</div> : null}
|
||||
{step.startedAt ? <div>{t('gitView.pr.checks.startedLabel')}: {formatTimestamp(step.startedAt)}</div> : null}
|
||||
{step.completedAt ? <div>{t('gitView.pr.checks.completedLabel')}: {formatTimestamp(step.completedAt)}</div> : null}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
@@ -808,7 +810,7 @@ export const PullRequestSection: React.FC<{
|
||||
setActiveMainTab('chat');
|
||||
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
if (!directory || !pr) return;
|
||||
@@ -827,7 +829,7 @@ export const PullRequestSection: React.FC<{
|
||||
});
|
||||
|
||||
if (failed.length === 0) {
|
||||
toast.message('No failed checks');
|
||||
toast.message(t('gitView.pr.toast.noFailedChecks'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -856,7 +858,7 @@ export const PullRequestSection: React.FC<{
|
||||
dispatchSyntheticPrompt(target, visibleText, instructionsText, payloadText);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load checks', { description: message });
|
||||
toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message });
|
||||
}
|
||||
}, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab]);
|
||||
|
||||
@@ -864,7 +866,7 @@ export const PullRequestSection: React.FC<{
|
||||
setActiveMainTab('chat');
|
||||
|
||||
if (!github?.prContext) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
if (!directory || !pr) return;
|
||||
@@ -879,7 +881,7 @@ export const PullRequestSection: React.FC<{
|
||||
const reviewComments = context.reviewComments ?? [];
|
||||
const total = issueComments.length + reviewComments.length;
|
||||
if (total === 0) {
|
||||
toast.message('No PR comments');
|
||||
toast.message(t('gitView.pr.toast.noPrComments'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -895,7 +897,7 @@ export const PullRequestSection: React.FC<{
|
||||
dispatchSyntheticPrompt(target, visibleText, instructionsText, payloadText);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to load PR comments', { description: message });
|
||||
toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message });
|
||||
}
|
||||
}, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab]);
|
||||
|
||||
@@ -1136,7 +1138,7 @@ export const PullRequestSection: React.FC<{
|
||||
onGeneratedDescription?.();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to generate description', { description: message });
|
||||
toast.error(t('gitView.pr.toast.generateDescriptionFailed'), { description: message });
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
@@ -1144,22 +1146,22 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
const createPr = React.useCallback(async () => {
|
||||
if (!github?.prCreate) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
const trimmedTitle = title.trim();
|
||||
if (!trimmedTitle) {
|
||||
toast.error('Title is required');
|
||||
toast.error(t('gitView.pr.toast.titleRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedBase = targetBaseBranch.trim();
|
||||
if (!trimmedBase) {
|
||||
toast.error('Base branch is required');
|
||||
toast.error(t('gitView.pr.toast.baseBranchRequired'));
|
||||
return;
|
||||
}
|
||||
if (trimmedBase === branch) {
|
||||
toast.error('Base branch must differ from head branch');
|
||||
toast.error(t('gitView.pr.toast.baseMustDifferFromHead'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1176,13 +1178,13 @@ export const PullRequestSection: React.FC<{
|
||||
draft,
|
||||
...(selectedRemote ? { remote: selectedRemote.name } : {}),
|
||||
});
|
||||
toast.success('PR created');
|
||||
toast.success(t('gitView.pr.toast.prCreated'));
|
||||
updatePrStatus(prStatusKey, (prev) => (prev ? { ...prev, pr } : prev));
|
||||
await refresh({ force: true });
|
||||
scheduleActionRefresh();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to create PR', { description: message });
|
||||
toast.error(t('gitView.pr.toast.createPrFailed'), { description: message });
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
@@ -1190,22 +1192,22 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
const mergePr = React.useCallback(async (pr: GitHubPullRequest) => {
|
||||
if (!github?.prMerge) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
setIsMerging(true);
|
||||
try {
|
||||
const result = await github.prMerge({ directory, number: pr.number, method: mergeMethod });
|
||||
if (result.merged) {
|
||||
toast.success('PR merged');
|
||||
toast.success(t('gitView.pr.toast.prMerged'));
|
||||
} else {
|
||||
toast.message('PR not merged', { description: result.message || 'Not mergeable' });
|
||||
toast.message(t('gitView.pr.toast.prNotMerged'), { description: result.message || t('gitView.pr.notMergeable') });
|
||||
}
|
||||
await refresh({ force: true });
|
||||
scheduleActionRefresh();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Merge failed', { description: message });
|
||||
toast.error(t('gitView.pr.toast.mergeFailed'), { description: message });
|
||||
if (pr.url) {
|
||||
void openExternal(pr.url);
|
||||
}
|
||||
@@ -1216,18 +1218,18 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
const markReady = React.useCallback(async (pr: GitHubPullRequest) => {
|
||||
if (!github?.prReady) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
setIsMarkingReady(true);
|
||||
try {
|
||||
await github.prReady({ directory, number: pr.number });
|
||||
toast.success('Marked ready for review');
|
||||
toast.success(t('gitView.pr.toast.markedReady'));
|
||||
await refresh({ force: true });
|
||||
scheduleActionRefresh();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to mark ready', { description: message });
|
||||
toast.error(t('gitView.pr.toast.markReadyFailed'), { description: message });
|
||||
if (pr.url) {
|
||||
void openExternal(pr.url);
|
||||
}
|
||||
@@ -1238,13 +1240,13 @@ export const PullRequestSection: React.FC<{
|
||||
|
||||
const updatePr = React.useCallback(async (pr: GitHubPullRequest) => {
|
||||
if (!github?.prUpdate) {
|
||||
toast.error('GitHub runtime API unavailable');
|
||||
toast.error(t('gitView.pr.toast.githubApiUnavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedTitle = editTitle.trim();
|
||||
if (!trimmedTitle) {
|
||||
toast.error('Title is required');
|
||||
toast.error(t('gitView.pr.toast.titleRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1266,12 +1268,12 @@ export const PullRequestSection: React.FC<{
|
||||
}
|
||||
: prev));
|
||||
setIsEditingPr(false);
|
||||
toast.success('PR updated');
|
||||
toast.success(t('gitView.pr.toast.prUpdated'));
|
||||
await refresh({ force: true });
|
||||
scheduleActionRefresh();
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error('Failed to update PR', { description: message });
|
||||
toast.error(t('gitView.pr.toast.updatePrFailed'), { description: message });
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
@@ -1312,17 +1314,17 @@ export const PullRequestSection: React.FC<{
|
||||
type="button"
|
||||
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background/70 hover:bg-interactive-hover/60"
|
||||
onClick={() => void openExternal(pr.url)}
|
||||
aria-label="Open PR on GitHub"
|
||||
aria-label={t('gitView.pr.actions.openOnGitHubAria')}
|
||||
>
|
||||
<PrStateIcon className="size-4 shrink-0" style={{ color: prColorVar }} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Open PR on GitHub</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.openOnGitHub')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<PrStateIcon className="size-4 shrink-0" style={{ color: 'var(--surface-muted-foreground)' }} />
|
||||
)}
|
||||
<h3 className="typography-ui-header font-semibold text-foreground truncate">Pull Request</h3>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground truncate">{t('gitView.pullRequest.title')}</h3>
|
||||
{pr ? (
|
||||
<span className="typography-meta text-muted-foreground truncate">#{pr.number}</span>
|
||||
) : null}
|
||||
@@ -1372,7 +1374,7 @@ export const PullRequestSection: React.FC<{
|
||||
<span style={{ color: prColorVar }}>
|
||||
{pr.state}{pr.draft ? ' (draft)' : ''}
|
||||
</span>
|
||||
{pr.mergeable === false ? ' · not mergeable' : ''}
|
||||
{pr.mergeable === false ? ` · ${t('gitView.pr.notMergeable')}` : ''}
|
||||
{pr.state === 'open' && typeof pr.mergeableState === 'string' && pr.mergeableState && pr.mergeableState !== 'unknown'
|
||||
? ` · ${pr.mergeableState}`
|
||||
: ''}
|
||||
@@ -1383,18 +1385,18 @@ export const PullRequestSection: React.FC<{
|
||||
<div className={bodyClassName}>
|
||||
{shouldShowConnectionNotice ? (
|
||||
<div className="space-y-2">
|
||||
<div className="typography-meta text-muted-foreground">
|
||||
GitHub not connected. Connect your GitHub account in settings.
|
||||
<div className="typography-meta text-muted-foreground">
|
||||
{t('gitView.pr.githubNotConnected')}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={openGitHubSettings} className="w-fit">
|
||||
Open settings
|
||||
{t('gitView.pr.actions.openSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="space-y-2">
|
||||
<div className="typography-ui-label text-foreground">PR status unavailable</div>
|
||||
<div className="typography-ui-label text-foreground">{t('gitView.pr.statusUnavailable')}</div>
|
||||
<div className="typography-meta text-muted-foreground break-words">{error}</div>
|
||||
{repoUrl ? (
|
||||
<Button variant="outline" size="sm" asChild className="w-fit">
|
||||
@@ -1410,7 +1412,7 @@ export const PullRequestSection: React.FC<{
|
||||
{!pr && !isInitialStatusResolved && !error && !shouldShowConnectionNotice ? (
|
||||
<div className="flex items-center gap-2 typography-micro text-muted-foreground">
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
Checking PR status...
|
||||
{t('gitView.pr.checkingStatus')}
|
||||
</div>
|
||||
) : pr ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -1421,7 +1423,7 @@ export const PullRequestSection: React.FC<{
|
||||
<Input
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
placeholder="PR title"
|
||||
placeholder={t('gitView.pr.placeholder.title')}
|
||||
autoCorrect={hasTouchInput ? "on" : "off"}
|
||||
autoCapitalize={hasTouchInput ? "sentences" : "off"}
|
||||
spellCheck={hasTouchInput}
|
||||
@@ -1430,7 +1432,7 @@ export const PullRequestSection: React.FC<{
|
||||
value={editBody}
|
||||
onChange={(e) => setEditBody(e.target.value)}
|
||||
className="min-h-[120px] bg-background/80"
|
||||
placeholder="Describe this PR"
|
||||
placeholder={t('gitView.pr.placeholder.description')}
|
||||
autoCorrect={hasTouchInput ? "on" : "off"}
|
||||
autoCapitalize={hasTouchInput ? "sentences" : "off"}
|
||||
spellCheck={hasTouchInput}
|
||||
@@ -1446,18 +1448,18 @@ export const PullRequestSection: React.FC<{
|
||||
/>
|
||||
) : (
|
||||
<div className="typography-micro text-muted-foreground whitespace-pre-wrap break-words mt-1">
|
||||
{isHydratingCurrentPrBody ? 'Loading description...' : 'No description provided.'}
|
||||
{isHydratingCurrentPrBody ? t('gitView.pr.loadingDescription') : t('gitView.pr.noDescription')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canMerge && pr.draft ? (
|
||||
<div className="typography-micro text-muted-foreground">
|
||||
Draft PRs must be marked ready before merge.
|
||||
{t('gitView.pr.draftMustBeReady')}
|
||||
</div>
|
||||
) : null}
|
||||
{!canMerge ? (
|
||||
<div className="typography-micro text-muted-foreground">No merge permission; use Open in GitHub.</div>
|
||||
<div className="typography-micro text-muted-foreground">{t('gitView.pr.noMergePermission')}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1478,12 +1480,12 @@ export const PullRequestSection: React.FC<{
|
||||
setEditBody(pr.body || '');
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
aria-label="Cancel editing"
|
||||
aria-label={t('gitView.pr.actions.cancelEditingAria')}
|
||||
>
|
||||
<RiCloseLine className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Cancel editing</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.cancelEditing')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -1492,12 +1494,12 @@ export const PullRequestSection: React.FC<{
|
||||
className="h-7 w-7 px-0"
|
||||
onClick={() => updatePr(pr)}
|
||||
disabled={isUpdating || !editTitle.trim()}
|
||||
aria-label="Save PR title and description"
|
||||
aria-label={t('gitView.pr.actions.savePrAria')}
|
||||
>
|
||||
{isUpdating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiCheckLine className="size-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Save PR title and description</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.savePr')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
@@ -1508,12 +1510,12 @@ export const PullRequestSection: React.FC<{
|
||||
size="sm"
|
||||
className="h-7 w-7 px-0"
|
||||
onClick={() => setIsEditingPr(true)}
|
||||
aria-label="Edit PR title and description"
|
||||
aria-label={t('gitView.pr.actions.editPrAria')}
|
||||
>
|
||||
<RiEditLine className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Edit PR title and description</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.editPr')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
) : null}
|
||||
@@ -1527,12 +1529,12 @@ export const PullRequestSection: React.FC<{
|
||||
className="h-7 w-7 px-0"
|
||||
onClick={openChecksDialog}
|
||||
disabled={isLoadingCheckDetails}
|
||||
aria-label="Open checks details"
|
||||
aria-label={t('gitView.pr.actions.openChecksAria')}
|
||||
>
|
||||
{isLoadingCheckDetails ? <RiLoader4Line className="size-4 animate-spin" /> : <RiInformationLine className="size-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Open checks details</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.openChecks')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
@@ -1544,12 +1546,12 @@ export const PullRequestSection: React.FC<{
|
||||
size="sm"
|
||||
className="h-7 w-7 px-0 border-[var(--status-success-border)] bg-[var(--status-success-background)] text-[var(--status-success)]"
|
||||
onClick={sendFailedChecksToChat}
|
||||
aria-label="Resolve failed checks with agent"
|
||||
aria-label={t('gitView.pr.actions.resolveFailedChecksAria')}
|
||||
>
|
||||
<RiErrorWarningLine className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Resolve failed checks with agent</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.resolveFailedChecks')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
@@ -1560,12 +1562,12 @@ export const PullRequestSection: React.FC<{
|
||||
size="sm"
|
||||
className="h-7 w-7 px-0"
|
||||
onClick={openCommentsDialog}
|
||||
aria-label="Open PR comments"
|
||||
aria-label={t('gitView.pr.actions.openCommentsAria')}
|
||||
>
|
||||
<RiChat4Line className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Open PR comments</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.openComments')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip delayDuration={300}>
|
||||
@@ -1575,12 +1577,12 @@ export const PullRequestSection: React.FC<{
|
||||
size="sm"
|
||||
className="h-7 w-7 px-0 border-[var(--status-success-border)] bg-[var(--status-success-background)] text-[var(--status-success)]"
|
||||
onClick={sendCommentsToChat}
|
||||
aria-label="Share comments with agent"
|
||||
aria-label={t('gitView.pr.actions.shareCommentsAria')}
|
||||
>
|
||||
<RiAiGenerate2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Share comments with agent</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.shareComments')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{canMerge && pr.draft && pr.state === 'open' ? (
|
||||
@@ -1592,12 +1594,12 @@ export const PullRequestSection: React.FC<{
|
||||
className="h-7 w-7 px-0"
|
||||
onClick={() => markReady(pr)}
|
||||
disabled={isMarkingReady || isMerging || isUpdating || isEditingPr}
|
||||
aria-label="Mark PR ready for review"
|
||||
aria-label={t('gitView.pr.actions.markReadyAria')}
|
||||
>
|
||||
{isMarkingReady ? <RiLoader4Line className="size-4 animate-spin" /> : <RiCheckboxCircleLine className="size-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Mark PR ready for review</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.markReady')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1614,9 +1616,9 @@ export const PullRequestSection: React.FC<{
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="squash">Squash</SelectItem>
|
||||
<SelectItem value="merge">Merge</SelectItem>
|
||||
<SelectItem value="rebase">Rebase</SelectItem>
|
||||
<SelectItem value="squash">{t('gitView.pr.mergeMethod.squash')}</SelectItem>
|
||||
<SelectItem value="merge">{t('gitView.pr.mergeMethod.merge')}</SelectItem>
|
||||
<SelectItem value="rebase">{t('gitView.pr.mergeMethod.rebase')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Tooltip delayDuration={300}>
|
||||
@@ -1626,12 +1628,12 @@ export const PullRequestSection: React.FC<{
|
||||
className="h-7 w-7 px-0"
|
||||
onClick={() => mergePr(pr)}
|
||||
disabled={isMerging || isMarkingReady || pr.state !== 'open' || pr.draft || isUpdating || isEditingPr}
|
||||
aria-label="Merge pull request"
|
||||
aria-label={t('gitView.pr.actions.mergePrAria')}
|
||||
>
|
||||
{isMerging ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitMergeLine className="size-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Merge pull request</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.mergePr')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : null}
|
||||
@@ -1643,7 +1645,7 @@ export const PullRequestSection: React.FC<{
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="typography-ui-label text-foreground">Create PR</div>
|
||||
<div className="typography-ui-label text-foreground">{t('gitView.pr.createTitle')}</div>
|
||||
<div className="typography-micro text-muted-foreground truncate">
|
||||
{branch} → {targetBaseBranch}
|
||||
</div>
|
||||
@@ -1652,18 +1654,18 @@ export const PullRequestSection: React.FC<{
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
|
||||
<RiExternalLinkLine className="size-4" />
|
||||
Repo
|
||||
{t('gitView.pr.actions.repo')}
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<label className="space-y-1">
|
||||
<div className="typography-micro text-muted-foreground">Title</div>
|
||||
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.title')}</div>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="PR title"
|
||||
placeholder={t('gitView.pr.placeholder.title')}
|
||||
autoCorrect={hasTouchInput ? "on" : "off"}
|
||||
autoCapitalize={hasTouchInput ? "sentences" : "off"}
|
||||
spellCheck={hasTouchInput}
|
||||
@@ -1671,11 +1673,11 @@ export const PullRequestSection: React.FC<{
|
||||
</label>
|
||||
|
||||
<label className="space-y-1">
|
||||
<div className="typography-micro text-muted-foreground">Base branch</div>
|
||||
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.baseBranch')}</div>
|
||||
{availableBaseBranches.length > 0 ? (
|
||||
<Select value={targetBaseBranch} onValueChange={setTargetBaseBranch}>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="Select base branch" />
|
||||
<SelectValue placeholder={t('gitView.pr.placeholder.selectBaseBranch')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableBaseBranches.map((candidate) => (
|
||||
@@ -1687,18 +1689,18 @@ export const PullRequestSection: React.FC<{
|
||||
<Input
|
||||
value={targetBaseBranch}
|
||||
onChange={(e) => setTargetBaseBranch(e.target.value)}
|
||||
placeholder="main"
|
||||
placeholder={t('gitView.pr.placeholder.main')}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
|
||||
<label className="space-y-1">
|
||||
<div className="typography-micro text-muted-foreground">Description</div>
|
||||
<div className="typography-micro text-muted-foreground">{t('gitView.pr.field.description')}</div>
|
||||
<Textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
className="min-h-[110px]"
|
||||
placeholder="What changed and why"
|
||||
placeholder={t('gitView.pr.placeholder.whatChanged')}
|
||||
autoCorrect={hasTouchInput ? "on" : "off"}
|
||||
autoCapitalize={hasTouchInput ? "sentences" : "off"}
|
||||
spellCheck={hasTouchInput}
|
||||
@@ -1722,9 +1724,9 @@ export const PullRequestSection: React.FC<{
|
||||
size="sm"
|
||||
checked={draft}
|
||||
onChange={(next) => setDraft(next)}
|
||||
ariaLabel="Toggle draft PR"
|
||||
ariaLabel={t('gitView.pr.actions.toggleDraftAria')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground select-none">Draft</span>
|
||||
<span className="typography-ui-label text-foreground select-none">{t('gitView.pr.field.draft')}</span>
|
||||
</div>
|
||||
|
||||
{/* Additional Context Section */}
|
||||
@@ -1732,20 +1734,20 @@ export const PullRequestSection: React.FC<{
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
Additional context (optional)
|
||||
{t('gitView.pr.additionalContext.optional')}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsContextSheetOpen(true)}
|
||||
>
|
||||
{additionalContext.trim() ? 'Edit' : 'Add'}
|
||||
{additionalContext.trim() ? t('gitView.pr.actions.edit') : t('gitView.pr.actions.add')}
|
||||
</Button>
|
||||
</div>
|
||||
{additionalContext.trim() && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center rounded-full bg-[var(--interactive-selection)] px-2 py-0.5 text-xs text-[var(--interactive-selection-foreground)]">
|
||||
Context added
|
||||
{t('gitView.pr.additionalContext.added')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1754,10 +1756,10 @@ export const PullRequestSection: React.FC<{
|
||||
<Collapsible open={isContextOpen} onOpenChange={setIsContextOpen}>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-3 py-2 hover:bg-[var(--interactive-hover)]">
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
Additional context (optional)
|
||||
{t('gitView.pr.additionalContext.optional')}
|
||||
</span>
|
||||
<span className="typography-micro text-[var(--primary-base)]">
|
||||
{isContextOpen ? 'Hide' : additionalContext.trim() ? 'Edit' : 'Add'}
|
||||
{isContextOpen ? t('gitView.pr.actions.hide') : additionalContext.trim() ? t('gitView.pr.actions.edit') : t('gitView.pr.actions.add')}
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
@@ -1766,10 +1768,10 @@ export const PullRequestSection: React.FC<{
|
||||
value={additionalContext}
|
||||
onChange={(e) => setAdditionalContext(e.target.value)}
|
||||
className="min-h-[100px] bg-transparent"
|
||||
placeholder="Explain why this change is needed... Mention how to test (commands / steps)... Call out risks / rollout plan..."
|
||||
placeholder={t('gitView.pr.placeholder.additionalContext')}
|
||||
/>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
This text is only used to guide PR generation.
|
||||
{t('gitView.pr.additionalContext.hint')}
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
@@ -1780,14 +1782,14 @@ export const PullRequestSection: React.FC<{
|
||||
<MobileOverlayPanel
|
||||
open={isContextSheetOpen}
|
||||
onClose={() => setIsContextSheetOpen(false)}
|
||||
title="Additional context"
|
||||
title={t('gitView.pr.additionalContext.title')}
|
||||
footer={
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setIsContextSheetOpen(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Done
|
||||
{t('gitView.common.done')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -1796,11 +1798,11 @@ export const PullRequestSection: React.FC<{
|
||||
value={additionalContext}
|
||||
onChange={(e) => setAdditionalContext(e.target.value)}
|
||||
className="min-h-[200px] bg-transparent"
|
||||
placeholder="Explain why this change is needed... Mention how to test (commands / steps)... Call out risks / rollout plan..."
|
||||
placeholder={t('gitView.pr.placeholder.additionalContext')}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
This text is only used to guide PR generation.
|
||||
{t('gitView.pr.additionalContext.hint')}
|
||||
</p>
|
||||
</div>
|
||||
</MobileOverlayPanel>
|
||||
@@ -1813,7 +1815,7 @@ export const PullRequestSection: React.FC<{
|
||||
disabled={isGenerating || isCreating}
|
||||
>
|
||||
{isGenerating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiAiGenerate2 className="size-4 text-primary" />}
|
||||
Generate
|
||||
{t('gitView.commit.generate')}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
@@ -1825,7 +1827,7 @@ export const PullRequestSection: React.FC<{
|
||||
<span className="inline-flex size-4 items-center justify-center">
|
||||
{isCreating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitPullRequestLine className="size-4" />}
|
||||
</span>
|
||||
<span>Create PR</span>
|
||||
<span>{t('gitView.pr.actions.createPr')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1837,10 +1839,10 @@ export const PullRequestSection: React.FC<{
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RiGitPullRequestLine className="h-5 w-5" />
|
||||
Check Details
|
||||
{t('gitView.pr.checkDetails.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{pr ? `PR #${pr.number}` : 'Pull request'}
|
||||
{pr ? t('gitView.pr.numberLabel', { number: pr.number }) : t('gitView.pullRequest.title')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -1848,7 +1850,7 @@ export const PullRequestSection: React.FC<{
|
||||
{isLoadingCheckDetails ? (
|
||||
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
|
||||
<RiLoader4Line className="h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
{t('gitView.loading.loading')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1864,7 +1866,7 @@ export const PullRequestSection: React.FC<{
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8">No check details available.</div>
|
||||
<div className="text-center text-muted-foreground py-8">{t('gitView.pr.checkDetails.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1878,9 +1880,9 @@ export const PullRequestSection: React.FC<{
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RiGitPullRequestLine className="h-5 w-5" />
|
||||
PR Comments
|
||||
{t('gitView.pr.comments.title')}
|
||||
{pr ? (
|
||||
<span className="typography-meta text-muted-foreground">PR #{pr.number}</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('gitView.pr.numberLabel', { number: pr.number })}</span>
|
||||
) : null}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -1889,7 +1891,7 @@ export const PullRequestSection: React.FC<{
|
||||
{isLoadingCommentsDetails ? (
|
||||
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
|
||||
<RiLoader4Line className="h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
{t('gitView.loading.loading')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1927,13 +1929,13 @@ export const PullRequestSection: React.FC<{
|
||||
onClick={() => {
|
||||
void sendSingleCommentToChat(comment);
|
||||
}}
|
||||
aria-label="Send this comment to agent"
|
||||
aria-label={t('gitView.pr.actions.sendCommentToAgentAria')}
|
||||
>
|
||||
<RiAiGenerate2 className="size-3.5" />
|
||||
Send to agent
|
||||
{t('gitView.pr.actions.sendToAgent')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Send this comment to agent</p></TooltipContent>
|
||||
<TooltipContent><p>{t('gitView.pr.actions.sendCommentToAgent')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="typography-micro text-muted-foreground">
|
||||
@@ -1955,7 +1957,7 @@ export const PullRequestSection: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8">No comments found.</div>
|
||||
<div className="text-center text-muted-foreground py-8">{t('gitView.pr.comments.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { toast } from '@/components/ui';
|
||||
import { RiAlertLine, RiLoader4Line } from '@remixicon/react';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface StashDialogProps {
|
||||
open: boolean;
|
||||
@@ -27,10 +28,11 @@ export const StashDialog: React.FC<StashDialogProps> = ({
|
||||
targetBranch,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [restoreAfter, setRestoreAfter] = React.useState(true);
|
||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||
|
||||
const operationLabel = operation === 'merge' ? 'Merge' : 'Rebase';
|
||||
const operationLabel = operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase');
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsProcessing(true);
|
||||
@@ -58,26 +60,25 @@ export const StashDialog: React.FC<StashDialogProps> = ({
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<RiAlertLine className="size-5 text-[var(--status-warning)]" />
|
||||
<DialogTitle>Uncommitted Changes</DialogTitle>
|
||||
<DialogTitle>{t('gitView.stash.title')}</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription>
|
||||
You have uncommitted changes that would be overwritten by this {operation}.
|
||||
Would you like to stash them temporarily?
|
||||
{t('gitView.stash.description', { operation })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-2">
|
||||
<p className="typography-meta text-muted-foreground mb-3">
|
||||
This will:
|
||||
{t('gitView.stash.thisWill')}
|
||||
</p>
|
||||
<ol className="list-decimal list-inside space-y-1 typography-meta text-foreground">
|
||||
<li>Stash your uncommitted changes</li>
|
||||
<li>{t('gitView.stash.stepStash')}</li>
|
||||
<li>
|
||||
{operation === 'merge' ? 'Merge' : 'Rebase'}{' '}
|
||||
{operation === 'merge' ? 'with' : 'onto'}{' '}
|
||||
{operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase')}{' '}
|
||||
{operation === 'merge' ? t('gitView.stash.mergeWith') : t('gitView.stash.rebaseOnto')}{' '}
|
||||
<span className="font-mono text-primary">{targetBranch}</span>
|
||||
</li>
|
||||
{restoreAfter && <li>Restore your stashed changes</li>}
|
||||
{restoreAfter && <li>{t('gitView.stash.stepRestore')}</li>}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -86,13 +87,13 @@ export const StashDialog: React.FC<StashDialogProps> = ({
|
||||
checked={restoreAfter}
|
||||
onChange={setRestoreAfter}
|
||||
disabled={isProcessing}
|
||||
ariaLabel="Restore changes after operation"
|
||||
ariaLabel={t('gitView.stash.restoreAria')}
|
||||
/>
|
||||
<span
|
||||
className="typography-ui-label text-foreground cursor-pointer select-none"
|
||||
onClick={() => !isProcessing && setRestoreAfter(!restoreAfter)}
|
||||
>
|
||||
Restore changes after the {operation}
|
||||
{t('gitView.stash.restoreAfterOperation', { operation })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +104,7 @@ export const StashDialog: React.FC<StashDialogProps> = ({
|
||||
onClick={handleCancel}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Cancel
|
||||
{t('gitView.common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -115,10 +116,10 @@ export const StashDialog: React.FC<StashDialogProps> = ({
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
Processing...
|
||||
{t('gitView.common.processing')}
|
||||
</>
|
||||
) : (
|
||||
`Stash & ${operationLabel}`
|
||||
t('gitView.stash.confirmButton', { operation: operationLabel })
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import type { GitRemote } from '@/lib/gitApi';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type SyncAction = 'fetch' | 'pull' | 'push' | null;
|
||||
|
||||
@@ -47,6 +48,7 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
|
||||
aheadCount = 0,
|
||||
behindCount = 0,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const skipRemoteSelectRef = React.useRef(false);
|
||||
const hasNoRemotes = remotes.length === 0;
|
||||
const isRemovingRemote = Boolean(removingRemoteName);
|
||||
@@ -192,8 +194,8 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
|
||||
event.stopPropagation();
|
||||
onRemoveRemote(remote);
|
||||
}}
|
||||
aria-label={`Remove ${remote.name} remote`}
|
||||
title={`Remove ${remote.name}`}
|
||||
aria-label={t('gitView.header.removeRemoteAria', { name: remote.name })}
|
||||
title={t('gitView.header.removeRemoteTitle', { name: remote.name })}
|
||||
>
|
||||
{removingRemoteName === remote.name ? (
|
||||
<RiLoader4Line className="size-3.5 animate-spin" />
|
||||
@@ -217,17 +219,17 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
|
||||
'fetch',
|
||||
<RiRefreshLine className="size-4" />,
|
||||
<RiLoader4Line className="size-4 animate-spin" />,
|
||||
'Fetch',
|
||||
t('gitView.sync.fetch'),
|
||||
onFetch,
|
||||
'Fetch from remote'
|
||||
t('gitView.sync.fetchTooltip')
|
||||
)
|
||||
: renderButton(
|
||||
'fetch',
|
||||
<RiRefreshLine className="size-4" />,
|
||||
<RiLoader4Line className="size-4 animate-spin" />,
|
||||
'Fetch',
|
||||
t('gitView.sync.fetch'),
|
||||
handleFetch,
|
||||
'Fetch from remote'
|
||||
t('gitView.sync.fetchTooltip')
|
||||
)}
|
||||
|
||||
{hasMultipleRemotes
|
||||
@@ -235,18 +237,22 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
|
||||
'pull',
|
||||
<RiArrowDownLine className="size-4" />,
|
||||
<RiLoader4Line className="size-4 animate-spin" />,
|
||||
'Pull',
|
||||
t('gitView.sync.pull'),
|
||||
onPull,
|
||||
behindCount > 0 ? `Pull changes (${behindCount} behind)` : 'Pull changes',
|
||||
behindCount > 0
|
||||
? t('gitView.sync.pullTooltipBehind', { count: behindCount })
|
||||
: t('gitView.sync.pullTooltip'),
|
||||
behindCount
|
||||
)
|
||||
: renderButton(
|
||||
'pull',
|
||||
<RiArrowDownLine className="size-4" />,
|
||||
<RiLoader4Line className="size-4 animate-spin" />,
|
||||
'Pull',
|
||||
t('gitView.sync.pull'),
|
||||
handlePull,
|
||||
behindCount > 0 ? `Pull changes (${behindCount} behind)` : 'Pull changes',
|
||||
behindCount > 0
|
||||
? t('gitView.sync.pullTooltipBehind', { count: behindCount })
|
||||
: t('gitView.sync.pullTooltip'),
|
||||
behindCount
|
||||
)}
|
||||
|
||||
@@ -254,9 +260,11 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
|
||||
'push',
|
||||
<RiArrowUpLine className="size-4" />,
|
||||
<RiLoader4Line className="size-4 animate-spin" />,
|
||||
'Push',
|
||||
t('gitView.sync.push'),
|
||||
handlePush,
|
||||
aheadCount > 0 ? `Push changes (${aheadCount} ahead)` : 'Push changes',
|
||||
aheadCount > 0
|
||||
? t('gitView.sync.pushTooltipAhead', { count: aheadCount })
|
||||
: t('gitView.sync.pushTooltip'),
|
||||
aheadCount
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RiGitBranchLine, RiEditLine, RiCheckLine, RiCloseLine, RiLoader4Line } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface WorktreeBranchDisplayProps {
|
||||
currentBranch: string | null | undefined;
|
||||
@@ -26,6 +27,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
|
||||
onRename,
|
||||
showEditButton = true,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [editBranchName, setEditBranchName] = React.useState(currentBranch || '');
|
||||
const [isRenaming, setIsRenaming] = React.useState(false);
|
||||
@@ -90,7 +92,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
|
||||
value={editBranchName}
|
||||
onChange={(e) => setEditBranchName(e.target.value)}
|
||||
className="flex-1 min-w-0 bg-transparent typography-ui-label outline-none placeholder:text-muted-foreground"
|
||||
placeholder="Branch name"
|
||||
placeholder={t('gitView.branch.namePlaceholder')}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
@@ -123,7 +125,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
|
||||
<RiGitBranchLine className="size-4 text-primary shrink-0" />
|
||||
<div className="inline-flex min-w-0 max-w-full items-center gap-1">
|
||||
<span className="truncate typography-ui-label font-normal text-foreground">
|
||||
{currentBranch || 'Detached HEAD'}
|
||||
{currentBranch || t('gitView.branch.detachedHead')}
|
||||
</span>
|
||||
{showEditButton && onRename && currentBranch && (
|
||||
<Button
|
||||
@@ -131,7 +133,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 shrink-0"
|
||||
onClick={handleStartEdit}
|
||||
title="Rename branch"
|
||||
title={t('gitView.branch.renameTitle')}
|
||||
>
|
||||
<RiEditLine className="size-4" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user