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:
Bohdan Triapitsyn
2026-04-26 14:03:39 +03:00
committed by GitHub
parent 87db2ea210
commit 7d7285655d
198 changed files with 24173 additions and 4365 deletions
@@ -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 &quot;Resolve with AI&quot; 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...&#10;Mention how to test (commands / steps)...&#10;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...&#10;Mention how to test (commands / steps)...&#10;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>