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
@@ -31,6 +31,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useI18n } from '@/lib/i18n';
interface AgentGroupDetailProps {
group: AgentGroup;
@@ -52,6 +53,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
group,
className,
}) => {
const { t } = useI18n();
const selectedSessionId = useAgentGroupsStore((s) => s.selectedSessionId);
const selectSession = useAgentGroupsStore((s) => s.selectSession);
const deleteGroupSessions = useAgentGroupsStore((s) => s.deleteGroupSessions);
@@ -92,17 +94,17 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
const handleCopyWorktreePath = React.useCallback(() => {
if (!selectedSession?.path) {
toast.error('No worktree path available');
toast.error(t('agentManager.detail.toast.noWorktreePath'));
return;
}
void copyTextToClipboard(selectedSession.path).then((result) => {
if (result.ok) {
toast.success('Worktree path copied');
toast.success(t('agentManager.detail.toast.worktreePathCopied'));
return;
}
toast.error('Failed to copy path');
toast.error(t('agentManager.detail.toast.failedToCopyPath'));
});
}, [selectedSession?.path]);
}, [selectedSession?.path, t]);
const handleRemoveSelectedWorktree = React.useCallback(() => {
if (!selectedSession) return;
@@ -123,24 +125,28 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
let sessionsToDelete: AgentGroupSession[];
if (worktreeDialog.kind === 'remove') {
toast.info('Removing worktree...');
toast.info(t('agentManager.detail.toast.removingWorktree'));
sessionsToDelete = group.sessions.filter((s) => normalize(s.path) === targetPath);
} else {
toast.info('Removing other worktrees...');
toast.info(t('agentManager.detail.toast.removingOtherWorktrees'));
sessionsToDelete = group.sessions.filter((s) => normalize(s.path) !== targetPath);
}
const { failedIds, failedWorktreePaths } = await deleteGroupSessions(sessionsToDelete, { removeWorktrees: true });
if (failedIds.length > 0 || failedWorktreePaths.length > 0) {
toast.error('Failed to fully remove worktree');
toast.error(t('agentManager.detail.toast.failedToFullyRemoveWorktree'));
} else {
toast.success(worktreeDialog.kind === 'remove' ? 'Worktree removed' : 'Removed other worktrees');
toast.success(
worktreeDialog.kind === 'remove'
? t('agentManager.detail.toast.worktreeRemoved')
: t('agentManager.detail.toast.otherWorktreesRemoved')
);
}
setWorktreeDialog(null);
} finally {
setIsProcessing(false);
}
}, [deleteGroupSessions, group.sessions, isProcessing, worktreeDialog]);
}, [deleteGroupSessions, group.sessions, isProcessing, t, worktreeDialog]);
// Group-level status: show if any session is busy
const allStatuses = useAllSessionStatuses();
@@ -160,11 +166,15 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
{groupBusy && <RiLoader4Line className="h-4 w-4 animate-spin text-amber-500 flex-shrink-0" />}
</div>
<div className="flex items-center gap-2 mt-1 typography-meta text-muted-foreground">
<span>{group.sessionCount} model{group.sessionCount !== 1 ? 's' : ''}</span>
<span>
{group.sessionCount === 1
? t('agentManager.detail.header.modelCountSingle', { count: group.sessionCount })
: t('agentManager.detail.header.modelCountPlural', { count: group.sessionCount })}
</span>
<span>·</span>
<span className="flex items-center gap-1">
<RiGitBranchLine className="h-3.5 w-3.5" />
{selectedSession?.worktreeMetadata?.label || selectedSession?.branch || 'No branch'}
{selectedSession?.worktreeMetadata?.label || selectedSession?.branch || t('agentManager.detail.header.noBranch')}
</span>
</div>
</div>
@@ -243,7 +253,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="flex-shrink-0" aria-label="Worktree actions">
<Button variant="outline" size="icon" className="flex-shrink-0" aria-label={t('agentManager.detail.actions.worktreeActionsAria')}>
<RiMore2Line className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -253,13 +263,13 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
closeOnClick={false}
variant="destructive"
>
Remove this worktree
{t('agentManager.detail.actions.removeThisWorktree')}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={handleKeepOnlySelectedWorktree}
closeOnClick={false}
>
Leave this one, remove others
{t('agentManager.detail.actions.keepThisRemoveOthers')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
@@ -269,7 +279,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
disabled={!selectedSession?.path}
>
<RiFileCopyLine className="h-4 w-4 mr-px" />
Copy Worktree Path
{t('agentManager.detail.actions.copyWorktreePath')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -281,24 +291,33 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
{worktreeDialog?.kind === 'remove' ? 'Remove worktree' : 'Remove other worktrees'}
{worktreeDialog?.kind === 'remove'
? t('agentManager.detail.dialog.removeWorktreeTitle')
: t('agentManager.detail.dialog.removeOtherWorktreesTitle')}
</DialogTitle>
<DialogDescription>
{worktreeDialog?.kind === 'remove'
? <>Remove <span className="text-foreground font-medium">{worktreeDialog?.label}</span>? This deletes all sessions in that worktree and removes the worktree itself.</>
: <>Keep <span className="text-foreground font-medium">{worktreeDialog?.label}</span> and remove the other worktrees in <span className="text-foreground font-medium">{group.name}</span>.</>}
? t('agentManager.detail.dialog.removeWorktreeDescription', { label: worktreeDialog?.label ?? '' })
: t('agentManager.detail.dialog.removeOtherWorktreesDescription', {
label: worktreeDialog?.label ?? '',
group: group.name,
})}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setWorktreeDialog(null)} disabled={isProcessing}>
Cancel
{t('agentManager.detail.dialog.cancel')}
</Button>
<Button
variant={worktreeDialog?.kind === 'remove' ? 'destructive' : 'default'}
onClick={() => void handleConfirmWorktreeAction()}
disabled={isProcessing}
>
{isProcessing ? 'Working…' : worktreeDialog?.kind === 'remove' ? 'Remove' : 'Remove others'}
{isProcessing
? t('agentManager.detail.dialog.working')
: worktreeDialog?.kind === 'remove'
? t('agentManager.detail.dialog.remove')
: t('agentManager.detail.dialog.removeOthers')}
</Button>
</DialogFooter>
</DialogContent>
@@ -328,10 +347,10 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<div className="flex-1 flex items-center justify-center">
<div className="text-center p-8">
<p className="typography-body text-muted-foreground mb-2">
Loading session for <span className="font-medium text-foreground">{selectedSession.displayLabel}</span>
{t('agentManager.detail.state.loadingSessionFor', { label: selectedSession.displayLabel })}
</p>
<p className="typography-micro text-muted-foreground/60">
Session ID: {selectedSession.id}
{t('agentManager.detail.state.sessionId', { id: selectedSession.id })}
</p>
</div>
</div>
@@ -340,7 +359,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
) : (
<div className="h-full flex items-center justify-center">
<p className="typography-body text-muted-foreground">
No sessions in this group
{t('agentManager.detail.state.noSessionsInGroup')}
</p>
</div>
)}
@@ -27,6 +27,7 @@ import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import type { ProjectRef } from '@/lib/openchamberConfig';
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
import { useI18n } from '@/lib/i18n';
/** Max file size in bytes (10MB) */
const MAX_FILE_SIZE = 10 * 1024 * 1024;
@@ -55,6 +56,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
onCreateGroup,
isCreating = false,
}) => {
const { t } = useI18n();
const [groupName, setGroupName] = React.useState('');
const [prompt, setPrompt] = React.useState('');
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
@@ -166,7 +168,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > MAX_FILE_SIZE) {
toast.error(`File "${file.name}" is too large (max 10MB)`);
toast.error(t('agentManager.empty.toast.fileTooLarge', { fileName: file.name }));
continue;
}
@@ -190,12 +192,16 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
attachedCount++;
} catch (error) {
console.error('File attach failed', error);
toast.error(`Failed to attach "${file.name}"`);
toast.error(t('agentManager.empty.toast.failedToAttach', { fileName: file.name }));
}
}
if (attachedCount > 0) {
toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
toast.success(
attachedCount === 1
? t('agentManager.empty.toast.attachedSingle', { count: attachedCount })
: t('agentManager.empty.toast.attachedPlural', { count: attachedCount })
);
}
if (fileInputRef.current) {
@@ -372,7 +378,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
setMentionQuery('');
} catch (error) {
console.error('Failed to create agent group:', error);
toast.error('Failed to create agent group');
toast.error(t('agentManager.empty.toast.failedToCreateGroup'));
} finally {
setIsSubmitting(false);
}
@@ -415,17 +421,17 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
{/* Group Name Input */}
<div className="space-y-1.5">
<label htmlFor="group-name" className="typography-ui-label font-medium text-foreground">
Group Name
{t('agentManager.empty.groupName.label')}
</label>
<Input
id="group-name"
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
placeholder="e.g. feature-auth, bugfix-login"
placeholder={t('agentManager.empty.groupName.placeholder')}
className="typography-body"
/>
<p className="typography-micro text-muted-foreground">
Used for worktree directory and branch naming
{t('agentManager.empty.groupName.description')}
</p>
</div>
@@ -433,7 +439,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
<div className="space-y-1.5">
<label className="typography-ui-label font-medium text-foreground flex items-center gap-1.5">
<RiGitBranchLine className="h-4 w-4 text-muted-foreground" />
Base Branch
{t('agentManager.empty.baseBranch.label')}
</label>
<BranchSelector
directory={currentDirectory}
@@ -441,7 +447,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
onChange={setBaseBranch}
/>
<p className="typography-micro text-muted-foreground">
Creates new branches from <code className="font-mono text-xs">{baseBranch}</code>
{t('agentManager.empty.baseBranch.description', { branch: baseBranch })}
</p>
</div>
@@ -449,10 +455,10 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:bg-[var(--interactive-hover)] rounded-md px-1 -mx-1 transition-colors">
<p className="typography-ui-label font-medium text-foreground">
Setup commands
{t('agentManager.empty.setupCommands.label')}
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
<span className="font-normal text-muted-foreground/70">
{' '}({setupCommands.filter(cmd => cmd.trim()).length} configured)
{' '}({t('agentManager.empty.setupCommands.configured', { count: setupCommands.filter(cmd => cmd.trim()).length })})
</span>
)}
</p>
@@ -464,10 +470,10 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
<CollapsibleContent>
<div className="pt-2 space-y-2">
<p className="typography-micro text-muted-foreground/70">
Commands run in each new worktree. Use <code className="font-mono text-xs">$ROOT_PROJECT_PATH</code> for project root.
{t('agentManager.empty.setupCommands.description')}
</p>
{isLoadingSetupCommands ? (
<p className="typography-meta text-muted-foreground/70">Loading...</p>
<p className="typography-meta text-muted-foreground/70">{t('agentManager.empty.setupCommands.loading')}</p>
) : (
<div className="space-y-1.5">
{setupCommands.map((command, index) => (
@@ -479,7 +485,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
newCommands[index] = e.target.value;
setSetupCommands(newCommands);
}}
placeholder="e.g., bun install"
placeholder={t('agentManager.empty.setupCommands.commandPlaceholder')}
className="h-8 flex-1 font-mono text-xs"
/>
<button
@@ -489,7 +495,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
setSetupCommands(newCommands);
}}
className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Remove command"
aria-label={t('agentManager.empty.setupCommands.removeCommandAria')}
>
<RiCloseLine className="h-4 w-4" />
</button>
@@ -501,7 +507,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors"
>
<RiAddLine className="h-3.5 w-3.5" />
Add command
{t('agentManager.empty.setupCommands.addCommand')}
</button>
</div>
)}
@@ -512,21 +518,21 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
{/* Agent Selection */}
<div className="space-y-1.5">
<label className="typography-ui-label font-medium text-foreground">
Agent
{t('agentManager.empty.agent.label')}
</label>
<AgentSelector
value={selectedAgent}
onChange={setSelectedAgent}
/>
<p className="typography-micro text-muted-foreground">
Defaults to your configured default agent
{t('agentManager.empty.agent.description')}
</p>
</div>
{/* Model Selection */}
<div className="space-y-1.5">
<label className="typography-ui-label font-medium text-foreground">
Models
{t('agentManager.empty.models.label')}
</label>
<ModelMultiSelect
selectedModels={selectedModels}
@@ -534,7 +540,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
onRemove={handleRemoveModel}
onUpdate={handleUpdateModel}
minModels={1}
addButtonLabel="Add model"
addButtonLabel={t('agentManager.empty.models.addModel')}
maxModels={5}
/>
</div>
@@ -542,7 +548,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
{/* Chat Input Style Prompt */}
<div className="space-y-1.5">
<label htmlFor="prompt" className="typography-ui-label font-medium text-foreground">
Prompt
{t('agentManager.empty.prompt.label')}
</label>
<div className="relative">
<div
@@ -561,7 +567,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
updateAutocompleteState(nextPrompt, cursorPosition);
}}
onKeyDown={handleKeyDown}
placeholder="Ask anything..."
placeholder={t('agentManager.empty.prompt.placeholder')}
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent dark:bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
/>
@@ -609,7 +615,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
type="button"
onClick={() => fileInputRef.current?.click()}
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground transition-colors"
aria-label="Add attachment"
aria-label={t('agentManager.empty.prompt.addAttachmentAria')}
>
<RiAddCircleLine className="h-[18px] w-[18px]" />
</button>
@@ -618,7 +624,9 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
{/* Right Controls - Model Count */}
<div className="flex items-center gap-2">
<span className="typography-meta text-muted-foreground">
{selectedModels.length} model{selectedModels.length !== 1 ? 's' : ''} selected
{selectedModels.length === 1
? t('agentManager.empty.models.selectedSingle', { count: selectedModels.length })
: t('agentManager.empty.models.selectedPlural', { count: selectedModels.length })}
</span>
</div>
{/* Submit Button */}
@@ -631,7 +639,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
? 'text-primary hover:text-primary'
: 'opacity-30'
)}
aria-label="Start Agent Group"
aria-label={t('agentManager.empty.actions.startAgentGroupAria')}
>
{isSubmittingOrCreating ? (
<RiHourglassFill className="h-[18px] w-[18px] animate-spin" />
@@ -28,8 +28,9 @@ import {
import { cn } from '@/lib/utils';
import { useAgentGroupsStore, type AgentGroup } from '@/stores/useAgentGroupsStore';
import { useAllSessionStatuses } from '@/sync/sync-context';
import { useI18n } from '@/lib/i18n';
const formatRelativeTime = (timestamp: number): string => {
const formatRelativeTime = (timestamp: number): { unit: 'now' | 'minutes' | 'hours' | 'days'; count?: number } => {
const now = Date.now();
const diff = now - timestamp;
@@ -37,10 +38,10 @@ const formatRelativeTime = (timestamp: number): string => {
const hours = Math.floor(diff / (60 * 60 * 1000));
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
if (minutes < 1) return 'now';
if (minutes < 60) return `${minutes}m`;
if (hours < 24) return `${hours}h`;
return `${days}d`;
if (minutes < 1) return { unit: 'now' };
if (minutes < 60) return { unit: 'minutes', count: minutes };
if (hours < 24) return { unit: 'hours', count: hours };
return { unit: 'days', count: days };
};
interface AgentGroupItemProps {
@@ -51,6 +52,7 @@ interface AgentGroupItemProps {
}
const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBusy, onSelect }) => {
const { t } = useI18n();
const [menuOpen, setMenuOpen] = React.useState(false);
const [confirmOpen, setConfirmOpen] = React.useState(false);
const [isDeleting, setIsDeleting] = React.useState(false);
@@ -59,16 +61,18 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
const handleDeleteGroup = React.useCallback(async () => {
if (isDeleting) return;
setIsDeleting(true);
toast.info(`Deleting "${group.name}"...`);
toast.info(t('agentManager.sidebar.toast.deletingGroup', { group: group.name }));
const { failedIds, failedWorktreePaths } = await deleteGroupSessions(group.sessions, { removeWorktrees: true });
if (failedIds.length === 0 && failedWorktreePaths.length === 0) {
toast.success(`Deleted "${group.name}"`);
toast.success(t('agentManager.sidebar.toast.deletedGroup', { group: group.name }));
} else {
toast.error(`Failed to fully delete "${group.name}"`);
toast.error(t('agentManager.sidebar.toast.failedToDeleteGroup', { group: group.name }));
}
setIsDeleting(false);
setConfirmOpen(false);
}, [deleteGroupSessions, group.name, group.sessions, isDeleting]);
}, [deleteGroupSessions, group.name, group.sessions, isDeleting, t]);
const relativeTime = formatRelativeTime(group.lastActive);
return (
<>
@@ -91,12 +95,20 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
{isBusy && <RiLoader4Line className="h-3 w-3 animate-spin text-amber-500 flex-shrink-0" />}
</div>
<div className="flex items-center gap-2">
<span className="typography-micro text-muted-foreground/60 flex items-center gap-1">
<RiGitBranchLine className="h-3 w-3" />
{group.sessionCount} model{group.sessionCount !== 1 ? 's' : ''}
</span>
<span className="typography-micro text-muted-foreground/60 flex items-center gap-1">
<RiGitBranchLine className="h-3 w-3" />
{group.sessionCount === 1
? t('agentManager.sidebar.item.modelCountSingle', { count: group.sessionCount })
: t('agentManager.sidebar.item.modelCountPlural', { count: group.sessionCount })}
</span>
<span className="typography-micro text-muted-foreground/60">
{formatRelativeTime(group.lastActive)}
{relativeTime.unit === 'now'
? t('agentManager.sidebar.relativeTime.now')
: relativeTime.unit === 'minutes'
? t('agentManager.sidebar.relativeTime.minutes', { count: relativeTime.count ?? 0 })
: relativeTime.unit === 'hours'
? t('agentManager.sidebar.relativeTime.hours', { count: relativeTime.count ?? 0 })
: t('agentManager.sidebar.relativeTime.days', { count: relativeTime.count ?? 0 })}
</span>
</div>
</button>
@@ -111,7 +123,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
'opacity-0 group-hover:opacity-100',
menuOpen && 'opacity-100',
)}
aria-label="Group menu"
aria-label={t('agentManager.sidebar.item.groupMenuAria')}
onClick={(e) => e.stopPropagation()}
>
<RiMore2Line className="h-3.5 w-3.5" />
@@ -126,7 +138,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
setConfirmOpen(true);
}}
>
Delete
{t('agentManager.sidebar.item.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -137,17 +149,17 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Delete agent group</DialogTitle>
<DialogTitle>{t('agentManager.sidebar.dialog.deleteGroupTitle')}</DialogTitle>
<DialogDescription>
Delete <span className="text-foreground font-medium">{group.name}</span>? This removes all worktrees and sessions in this group.
{t('agentManager.sidebar.dialog.deleteGroupDescription', { group: group.name })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmOpen(false)} disabled={isDeleting}>
Cancel
{t('agentManager.sidebar.dialog.cancel')}
</Button>
<Button variant="destructive" onClick={() => void handleDeleteGroup()} disabled={isDeleting}>
{isDeleting ? 'Deleting…' : 'Delete'}
{isDeleting ? t('agentManager.sidebar.dialog.deleting') : t('agentManager.sidebar.dialog.delete')}
</Button>
</DialogFooter>
</DialogContent>
@@ -171,6 +183,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
onGroupSelect,
onNewAgent,
}) => {
const { t } = useI18n();
const [searchQuery, setSearchQuery] = React.useState('');
const [showAll, setShowAll] = React.useState(false);
const isLoading = useAgentGroupsStore((s) => s.isLoading);
@@ -209,7 +222,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search Agent Groups..."
placeholder={t('agentManager.sidebar.search.placeholder')}
className="pl-8 h-8 rounded-lg border-border/40 bg-background/50 typography-meta"
/>
</div>
@@ -223,7 +236,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
onClick={onNewAgent}
>
<RiAddLine className="h-4 w-4" />
<span className="typography-ui-label">New Agent Group</span>
<span className="typography-ui-label">{t('agentManager.sidebar.actions.newAgentGroup')}</span>
</Button>
</div>
@@ -231,11 +244,11 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
<div className="px-2.5 py-1.5 flex items-center gap-1">
<RiArrowDownSLine className="h-4 w-4 text-muted-foreground" />
<span className="typography-micro font-medium text-muted-foreground uppercase tracking-wider">
Agent Groups
{t('agentManager.sidebar.section.agentGroups')}
</span>
{isLoading && (
<span className="typography-micro text-muted-foreground/50 ml-auto">
Loading...
{t('agentManager.sidebar.state.loading')}
</span>
)}
</div>
@@ -261,7 +274,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
onClick={() => setShowAll(true)}
className="mt-1 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left typography-micro text-muted-foreground/70 hover:text-foreground hover:underline"
>
... More ({remainingCount})
{t('agentManager.sidebar.actions.more', { count: remainingCount })}
</button>
)}
@@ -271,18 +284,18 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
onClick={() => setShowAll(false)}
className="mt-1 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left typography-micro text-muted-foreground/70 hover:text-foreground hover:underline"
>
Show less
{t('agentManager.sidebar.actions.showLess')}
</button>
)}
{!isLoading && filteredGroups.length === 0 && (
<div className="py-4 text-center">
<p className="typography-meta text-muted-foreground">
{searchQuery.trim() ? 'No groups found' : 'No agent groups yet'}
{searchQuery.trim() ? t('agentManager.sidebar.state.noGroupsFound') : t('agentManager.sidebar.state.noGroupsYet')}
</p>
{!searchQuery.trim() && (
<p className="typography-micro text-muted-foreground/60 mt-1">
Create a new agent group to get started
{t('agentManager.sidebar.state.createToGetStarted')}
</p>
)}
</div>