Add i18n foundation and translations (#1027)
* feat: add i18n foundation * feat: localize sessions sidebar * Localize multirun/scheduled tasks and fix dialog dropdown interactions * localize git sidebar surface and add zh-CN keys * feat(ui): localize context panel, diff/plan views, and context sidebar content * fix(config): resolve user config home via fs/home before embedded home * localize header/chat UI and complete model/worktree panel strings * localize worktree + github issue/pr dialog flows * localize settings sections and split settings i18n dictionaries * localize additional settings sections and sidebars * localize more settings pages and dialogs * fix settings select trigger localization * localize tunnel settings ui surface * localize additional settings sections * localize keyboard shortcuts labels in settings * localize terminal and utility dialogs surfaces * feat(i18n): localize remaining UI strings * Add Ukrainian locale * Add Spanish locale * Add Brazilian Portuguese locale * Polish locale translations
This commit is contained in:
committed by
GitHub
parent
87db2ea210
commit
7d7285655d
@@ -55,6 +55,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useModelLists } from '@/hooks/useModelLists';
|
||||
import { useIsTextTruncated } from '@/hooks/useIsTextTruncated';
|
||||
import type { MobileControlsPanel } from './mobileControlsUtils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type IconComponent = ComponentType<any>;
|
||||
@@ -293,6 +294,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
onMobilePanelSelection,
|
||||
onAgentPanelSelection,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
const currentProviderId = useConfigStore((state) => state.currentProviderId);
|
||||
const currentModelId = useConfigStore((state) => state.currentModelId);
|
||||
@@ -545,9 +547,29 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
const currentMetadata =
|
||||
currentProviderId && currentModelId ? getModelMetadata(currentProviderId, currentModelId) : undefined;
|
||||
const currentCapabilityIcons = getCapabilityIcons(currentMetadata);
|
||||
const inputModalityIcons = getModalityIcons(currentMetadata, 'input');
|
||||
const outputModalityIcons = getModalityIcons(currentMetadata, 'output');
|
||||
const localizeMetaLabel = React.useCallback((label: string) => {
|
||||
if (label === 'Tool calling') return t('chat.modelControls.capability.toolCalling');
|
||||
if (label === 'Reasoning') return t('chat.modelControls.capability.reasoning');
|
||||
if (label === 'Text') return t('chat.modelControls.modality.text');
|
||||
if (label === 'Image') return t('chat.modelControls.modality.image');
|
||||
if (label === 'Video') return t('chat.modelControls.modality.video');
|
||||
if (label === 'Audio') return t('chat.modelControls.modality.audio');
|
||||
if (label === 'PDF') return t('chat.modelControls.modality.pdf');
|
||||
return label;
|
||||
}, [t]);
|
||||
|
||||
const currentCapabilityIcons = React.useMemo(
|
||||
() => getCapabilityIcons(currentMetadata).map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) })),
|
||||
[currentMetadata, localizeMetaLabel],
|
||||
);
|
||||
const inputModalityIcons = React.useMemo(
|
||||
() => getModalityIcons(currentMetadata, 'input').map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) })),
|
||||
[currentMetadata, localizeMetaLabel],
|
||||
);
|
||||
const outputModalityIcons = React.useMemo(
|
||||
() => getModalityIcons(currentMetadata, 'output').map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) })),
|
||||
[currentMetadata, localizeMetaLabel],
|
||||
);
|
||||
|
||||
// Compute from current model each render to avoid stale variants
|
||||
// in draft/session transitions.
|
||||
@@ -1148,14 +1170,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{}
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-0.5">Provider</div>
|
||||
<div className="typography-micro text-muted-foreground mb-0.5">{t('chat.modelControls.provider')}</div>
|
||||
<div className="typography-meta text-foreground font-medium">{getProviderDisplayName()}</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{currentCapabilityIcons.length > 0 && (
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-1">Capabilities</div>
|
||||
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.capabilities')}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{currentCapabilityIcons.map(({ key, icon, label }) => (
|
||||
<div key={key} className="flex items-center gap-1.5">
|
||||
@@ -1170,11 +1192,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
{}
|
||||
{(inputModalityIcons.length > 0 || outputModalityIcons.length > 0) && (
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-1">Modalities</div>
|
||||
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.modalities')}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{inputModalityIcons.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="typography-meta text-muted-foreground/80 w-12">Input</span>
|
||||
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.input')}</span>
|
||||
<div className="flex gap-1">
|
||||
{inputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} input`, `input-${key}`))}
|
||||
</div>
|
||||
@@ -1182,7 +1204,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
)}
|
||||
{outputModalityIcons.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="typography-meta text-muted-foreground/80 w-12">Output</span>
|
||||
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.output')}</span>
|
||||
<div className="flex gap-1">
|
||||
{outputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} output`, `output-${key}`))}
|
||||
</div>
|
||||
@@ -1194,14 +1216,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
{}
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-1">Limits</div>
|
||||
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.limits')}</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Context</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.context')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{formatTokens(currentMetadata?.limit?.context)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Output</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.output')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{formatTokens(currentMetadata?.limit?.output)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1209,14 +1231,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
{}
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-1">Metadata</div>
|
||||
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.metadata')}</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Knowledge</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.knowledge')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{formatKnowledge(currentMetadata?.knowledge)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Release</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.release')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{formatDate(currentMetadata?.release_date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1239,12 +1261,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
const action = resolveWildcardPermissionAction(rules, permissionName) ?? 'ask';
|
||||
|
||||
if (hasCustom) {
|
||||
return { mode: 'ask', label: 'Custom' };
|
||||
return { mode: 'ask', label: t('chat.modelControls.permissionLabel.custom') };
|
||||
}
|
||||
|
||||
if (action === 'allow') return { mode: 'allow', label: 'Allow' };
|
||||
if (action === 'deny') return { mode: 'deny', label: 'Deny' };
|
||||
return { mode: 'ask', label: 'Ask' };
|
||||
if (action === 'allow') return { mode: 'allow', label: t('chat.modelControls.permissionLabel.allow') };
|
||||
if (action === 'deny') return { mode: 'deny', label: t('chat.modelControls.permissionLabel.deny') };
|
||||
return { mode: 'ask', label: t('chat.modelControls.permissionLabel.ask') };
|
||||
};
|
||||
|
||||
const editPermissionSummary = summarizePermission('edit');
|
||||
@@ -1267,16 +1289,22 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
{}
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-0.5">Mode</div>
|
||||
<div className="typography-micro text-muted-foreground mb-0.5">{t('chat.modelControls.mode')}</div>
|
||||
<div className="typography-meta text-foreground font-medium">
|
||||
{currentAgent.mode === 'primary' ? 'Primary' : currentAgent.mode === 'subagent' ? 'Subagent' : currentAgent.mode === 'all' ? 'All' : '—'}
|
||||
{currentAgent.mode === 'primary'
|
||||
? t('chat.modelControls.modeValue.primary')
|
||||
: currentAgent.mode === 'subagent'
|
||||
? t('chat.modelControls.modeValue.subagent')
|
||||
: currentAgent.mode === 'all'
|
||||
? t('chat.modelControls.modeValue.all')
|
||||
: t('chat.modelControls.modeValue.none')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{(hasModelConfig || hasTemperatureOrTopP) && (
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-1">Model</div>
|
||||
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.model')}</div>
|
||||
{hasModelConfig && (
|
||||
<div className="typography-meta text-foreground font-medium mb-1">
|
||||
{currentAgent.model!.providerID} / {currentAgent.model!.modelID}
|
||||
@@ -1286,13 +1314,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{currentAgent.temperature !== undefined && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Temperature</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.temperature')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{currentAgent.temperature}</span>
|
||||
</div>
|
||||
)}
|
||||
{currentAgent.topP !== undefined && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Top P</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.topP')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{currentAgent.topP}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1304,10 +1332,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
{}
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground mb-1">Permissions</div>
|
||||
<div className="typography-micro text-muted-foreground mb-1">{t('chat.modelControls.permissions')}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Edit</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.edit')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')}
|
||||
<span className="typography-meta font-medium text-foreground">
|
||||
@@ -1316,7 +1344,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Bash</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.bash')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')}
|
||||
<span className="typography-meta font-medium text-foreground">
|
||||
@@ -1325,7 +1353,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">WebFetch</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.webFetch')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')}
|
||||
<span className="typography-meta font-medium text-foreground">
|
||||
@@ -1340,7 +1368,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
{hasCustomPrompt && (
|
||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground/80">Custom Prompt</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
|
||||
<RiCheckboxCircleLine className="h-4 w-4 text-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1408,7 +1436,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<MobileOverlayPanel
|
||||
open={activeMobilePanel === 'model'}
|
||||
onClose={closeMobilePanel}
|
||||
title="Select model"
|
||||
title={t('chat.modelControls.selectModel')}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
@@ -1417,7 +1445,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<Input
|
||||
value={mobileModelQuery}
|
||||
onChange={(event) => setMobileModelQuery(event.target.value)}
|
||||
placeholder="Search providers or models"
|
||||
placeholder={t('chat.modelControls.searchProvidersOrModels')}
|
||||
className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
|
||||
/>
|
||||
{mobileModelQuery && (
|
||||
@@ -1425,7 +1453,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
type="button"
|
||||
onClick={() => setMobileModelQuery('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Clear search"
|
||||
aria-label={t('chat.modelControls.clearSearch')}
|
||||
>
|
||||
<RiCloseCircleLine className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -1444,7 +1472,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-primary" />
|
||||
Favorites
|
||||
{t('chat.modelControls.favorites')}
|
||||
</div>
|
||||
<div className="flex flex-col border-t border-border/30">
|
||||
{favoriteModelsList.map(({ model, providerID, modelID }) => {
|
||||
@@ -1490,7 +1518,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
<RiTimeLine className="h-3 w-3 inline-block mr-1.5" />
|
||||
Recent
|
||||
{t('chat.modelControls.recent')}
|
||||
</div>
|
||||
<div className="flex flex-col border-t border-border/30">
|
||||
{recentModelsList.map(({ model, providerID, modelID }) => {
|
||||
@@ -1556,7 +1584,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
{provider.name}
|
||||
</span>
|
||||
{isActiveProvider && (
|
||||
<span className="typography-micro text-primary/80">Current</span>
|
||||
<span className="typography-micro text-primary/80">{t('chat.modelControls.current')}</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
@@ -1571,8 +1599,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
{providerModels.map((model: ProviderModel) => {
|
||||
const isSelected = isActiveProvider && model.id === currentModelId;
|
||||
const metadata = getModelMetadata(provider.id, model.id!);
|
||||
const capabilityIcons = getCapabilityIcons(metadata).slice(0, 3);
|
||||
const inputIcons = getModalityIcons(metadata, 'input');
|
||||
const capabilityIcons = getCapabilityIcons(metadata).slice(0, 3).map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) }));
|
||||
const inputIcons = getModalityIcons(metadata, 'input').map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) }));
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1636,8 +1664,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
)}
|
||||
aria-label={isFavoriteModel(provider.id as string, model.id as string) ? "Unfavorite" : "Favorite"}
|
||||
title={isFavoriteModel(provider.id as string, model.id as string) ? "Remove from favorites" : "Add to favorites"}
|
||||
aria-label={isFavoriteModel(provider.id as string, model.id as string)
|
||||
? t('chat.modelControls.unfavoriteAria')
|
||||
: t('chat.modelControls.favoriteAria')}
|
||||
title={isFavoriteModel(provider.id as string, model.id as string)
|
||||
? t('chat.modelControls.removeFromFavorites')
|
||||
: t('chat.modelControls.addToFavorites')}
|
||||
>
|
||||
{isFavoriteModel(provider.id as string, model.id as string) ? (
|
||||
<RiStarFill className="h-4 w-4" />
|
||||
@@ -1682,7 +1714,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<MobileOverlayPanel
|
||||
open={activeMobilePanel === 'variant'}
|
||||
onClose={closeMobilePanel}
|
||||
title="Thinking"
|
||||
title={t('chat.modelControls.thinking')}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<button
|
||||
@@ -1694,7 +1726,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
)}
|
||||
onClick={() => handleSelect(undefined)}
|
||||
>
|
||||
<span className="typography-meta font-medium text-foreground">Default</span>
|
||||
<span className="typography-meta font-medium text-foreground">{t('chat.modelControls.default')}</span>
|
||||
{isDefault && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</button>
|
||||
|
||||
@@ -1730,7 +1762,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<MobileOverlayPanel
|
||||
open={activeMobilePanel === 'agent'}
|
||||
onClose={closeMobilePanel}
|
||||
title="Select agent"
|
||||
title={t('chat.modelControls.selectAgent')}
|
||||
contentMaxHeightClassName="max-h-[min(52dvh,360px)]"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -1788,22 +1820,22 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<span className="typography-meta text-muted-foreground">{getProviderDisplayName()}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Capabilities</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.capabilities')}</span>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{currentCapabilityIcons.length > 0 ? (
|
||||
currentCapabilityIcons.map(({ key, icon, label }) =>
|
||||
renderIconBadge(icon, label, `cap-${key}`)
|
||||
)
|
||||
) : (
|
||||
<span className="typography-meta text-muted-foreground">—</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('chat.modelControls.modeValue.none')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Modalities</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.modalities')}</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">Input</span>
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">{t('chat.modelControls.input')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{inputModalityIcons.length > 0
|
||||
? inputModalityIcons.map(({ key, icon, label }) =>
|
||||
@@ -1813,7 +1845,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">Output</span>
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">{t('chat.modelControls.output')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{outputModalityIcons.length > 0
|
||||
? outputModalityIcons.map(({ key, icon, label }) =>
|
||||
@@ -1825,7 +1857,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Cost ($/1M tokens)</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.costPerMillion')}</span>
|
||||
{costRows.map((row) => (
|
||||
<div key={row.label} className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">{row.label}</span>
|
||||
@@ -1834,7 +1866,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Limits</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.limits')}</span>
|
||||
{limitRows.map((row) => (
|
||||
<div key={row.label} className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">{row.label}</span>
|
||||
@@ -1843,19 +1875,19 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Metadata</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.metadata')}</span>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">Knowledge</span>
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">{t('chat.modelControls.knowledge')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{formatKnowledge(currentMetadata.knowledge)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">Release</span>
|
||||
<span className="typography-meta font-medium text-muted-foreground/80">{t('chat.modelControls.release')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{formatDate(currentMetadata.release_date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-[200px] typography-meta text-muted-foreground">Model metadata unavailable.</div>
|
||||
<div className="min-w-[200px] typography-meta text-muted-foreground">{t('chat.modelControls.metadataUnavailable')}</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
);
|
||||
@@ -1872,11 +1904,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
const metadata = getModelMetadata(providerID, modelID);
|
||||
const capabilityIcons = getCapabilityIcons(metadata).map((icon) => ({
|
||||
...icon,
|
||||
label: localizeMetaLabel(icon.label),
|
||||
id: `cap-${icon.key}`,
|
||||
}));
|
||||
const modalityIcons = [
|
||||
...getModalityIcons(metadata, 'input'),
|
||||
...getModalityIcons(metadata, 'output'),
|
||||
...getModalityIcons(metadata, 'input').map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) })),
|
||||
...getModalityIcons(metadata, 'output').map((icon) => ({ ...icon, label: localizeMetaLabel(icon.label) })),
|
||||
];
|
||||
const uniqueModalityIcons = Array.from(
|
||||
new Map(modalityIcons.map((icon) => [icon.key, icon])).values()
|
||||
@@ -2006,8 +2039,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
|
||||
isFavorite ? "text-primary" : "text-muted-foreground"
|
||||
)}
|
||||
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
|
||||
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
||||
aria-label={isFavorite
|
||||
? t('chat.modelControls.unfavoriteAria')
|
||||
: t('chat.modelControls.favoriteAria')}
|
||||
title={isFavorite
|
||||
? t('chat.modelControls.removeFromFavorites')
|
||||
: t('chat.modelControls.addToFavorites')}
|
||||
>
|
||||
{isFavorite ? (
|
||||
<RiStarFill className="h-3.5 w-3.5" />
|
||||
@@ -2229,7 +2266,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search models"
|
||||
placeholder={t('chat.modelControls.searchModels')}
|
||||
value={desktopModelQuery}
|
||||
onChange={(e) => setDesktopModelQuery(e.target.value)}
|
||||
onKeyDown={handleModelKeyDown}
|
||||
@@ -2260,14 +2297,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<span className="flex h-4 w-4 items-center justify-center text-muted-foreground">
|
||||
<RiAddLine className="h-4 w-4 -mr-0.5" />
|
||||
</span>
|
||||
<span className="font-medium text-foreground">Add new provider</span>
|
||||
<span className="font-medium text-foreground">{t('chat.modelControls.addNewProvider')}</span>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{!hasResults && (
|
||||
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
|
||||
No models found
|
||||
{t('chat.modelControls.noModelsFound')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2278,7 +2315,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
|
||||
>
|
||||
<RiStarFill className="h-4 w-4 text-primary" />
|
||||
Favorites
|
||||
{t('chat.modelControls.favorites')}
|
||||
</DropdownMenuLabel>
|
||||
{filteredFavorites.map(({ model, providerID, modelID }) => {
|
||||
const idx = currentFlatIndex++;
|
||||
@@ -2295,7 +2332,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
|
||||
>
|
||||
<RiTimeLine className="h-4 w-4" />
|
||||
Recent
|
||||
{t('chat.modelControls.recent')}
|
||||
</DropdownMenuLabel>
|
||||
{filteredRecents.map(({ model, providerID, modelID }) => {
|
||||
const idx = currentFlatIndex++;
|
||||
@@ -2340,7 +2377,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
forceExpandProviders ? 'cursor-default' : 'cursor-pointer'
|
||||
)}
|
||||
aria-expanded={isExpanded}
|
||||
title={forceExpandProviders ? undefined : (isExpanded ? 'Collapse provider' : 'Expand provider')}
|
||||
title={forceExpandProviders
|
||||
? undefined
|
||||
: (isExpanded
|
||||
? t('chat.modelControls.collapseProvider')
|
||||
: t('chat.modelControls.expandProvider'))}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<ProviderLogo
|
||||
@@ -2368,7 +2409,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
{/* Keyboard hints footer */}
|
||||
<div className="px-3 pt-1 pb-1.5 border-t border-border/40 typography-micro text-muted-foreground">
|
||||
↑↓ navigate{highlightedSupportsThinking ? ' • ←→ thinking' : ''} • Enter select • Esc close
|
||||
{t('chat.modelControls.keyboardHint', {
|
||||
thinking: highlightedSupportsThinking ? ` • ${t('chat.modelControls.keyboardHintThinking')}` : '',
|
||||
})}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -2415,7 +2458,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
if (!currentAgent) {
|
||||
return (
|
||||
<TooltipContent align="start" sideOffset={8} className="max-w-[320px]">
|
||||
<div className="min-w-[200px] typography-meta text-muted-foreground">No agent selected.</div>
|
||||
<div className="min-w-[200px] typography-meta text-muted-foreground">{t('chat.modelControls.noAgentSelected')}</div>
|
||||
</TooltipContent>
|
||||
);
|
||||
}
|
||||
@@ -2430,12 +2473,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
const action = resolveWildcardPermissionAction(rules, permissionName) ?? 'ask';
|
||||
|
||||
if (hasCustom) {
|
||||
return { mode: 'ask', label: 'Custom' };
|
||||
}
|
||||
return { mode: 'ask', label: t('chat.modelControls.permissionLabel.custom') };
|
||||
}
|
||||
|
||||
if (action === 'allow') return { mode: 'allow', label: 'Allow' };
|
||||
if (action === 'deny') return { mode: 'deny', label: 'Deny' };
|
||||
return { mode: 'ask', label: 'Ask' };
|
||||
if (action === 'allow') return { mode: 'allow', label: t('chat.modelControls.permissionLabel.allow') };
|
||||
if (action === 'deny') return { mode: 'deny', label: t('chat.modelControls.permissionLabel.deny') };
|
||||
return { mode: 'ask', label: t('chat.modelControls.permissionLabel.ask') };
|
||||
};
|
||||
|
||||
const editPermissionSummary = summarizePermission('edit');
|
||||
@@ -2455,33 +2498,39 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Mode</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.mode')}</span>
|
||||
<span className="typography-meta text-foreground">
|
||||
{currentAgent.mode === 'primary' ? 'Primary' : currentAgent.mode === 'subagent' ? 'Subagent' : currentAgent.mode === 'all' ? 'All' : '—'}
|
||||
{currentAgent.mode === 'primary'
|
||||
? t('chat.modelControls.modeValue.primary')
|
||||
: currentAgent.mode === 'subagent'
|
||||
? t('chat.modelControls.modeValue.subagent')
|
||||
: currentAgent.mode === 'all'
|
||||
? t('chat.modelControls.modeValue.all')
|
||||
: t('chat.modelControls.modeValue.none')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(hasModelConfig || hasTemperatureOrTopP) && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Model</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.model')}</span>
|
||||
{hasModelConfig ? (
|
||||
<span className="typography-meta text-foreground">
|
||||
{currentAgent.model!.providerID} / {currentAgent.model!.modelID}
|
||||
</span>
|
||||
) : (
|
||||
<span className="typography-meta text-muted-foreground">—</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('chat.modelControls.modeValue.none')}</span>
|
||||
)}
|
||||
{hasTemperatureOrTopP && (
|
||||
<div className="flex flex-col gap-0.5 mt-0.5">
|
||||
{currentAgent.temperature !== undefined && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta text-muted-foreground/80">Temperature</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.temperature')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{currentAgent.temperature}</span>
|
||||
</div>
|
||||
)}
|
||||
{currentAgent.topP !== undefined && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta text-muted-foreground/80">Top P</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.topP')}</span>
|
||||
<span className="typography-meta font-medium text-foreground">{currentAgent.topP}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -2492,9 +2541,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">Permissions</span>
|
||||
<span className="typography-meta font-semibold uppercase tracking-wide text-muted-foreground/90">{t('chat.modelControls.permissions')}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="typography-meta text-muted-foreground/80 w-16">Edit</span>
|
||||
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.edit')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')}
|
||||
<span className="typography-meta font-medium text-foreground w-12">
|
||||
@@ -2503,7 +2552,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="typography-meta text-muted-foreground/80 w-16">Bash</span>
|
||||
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.bash')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')}
|
||||
<span className="typography-meta font-medium text-foreground w-12">
|
||||
@@ -2512,7 +2561,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="typography-meta text-muted-foreground/80 w-16">WebFetch</span>
|
||||
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.webFetch')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')}
|
||||
<span className="typography-meta font-medium text-foreground w-12">
|
||||
@@ -2524,7 +2573,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
{hasCustomPrompt && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="typography-meta text-muted-foreground/80">Custom Prompt</span>
|
||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
|
||||
<RiCheckboxCircleLine className="h-4 w-4 text-foreground" />
|
||||
</div>
|
||||
)}
|
||||
@@ -2538,7 +2587,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayVariant = currentVariant ?? 'Default';
|
||||
const displayVariant = currentVariant ?? t('chat.modelControls.default');
|
||||
const isDefault = !currentVariant;
|
||||
const colorClass = isDefault ? 'text-muted-foreground' : 'text-[color:var(--status-info)]';
|
||||
|
||||
@@ -2594,10 +2643,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(180px,calc(100vw-2rem))]">
|
||||
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">Thinking</DropdownMenuLabel>
|
||||
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">{t('chat.modelControls.thinking')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
|
||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||
<span className="typography-meta font-medium text-foreground truncate min-w-0">Default</span>
|
||||
<span className="typography-meta font-medium text-foreground truncate min-w-0">{t('chat.modelControls.default')}</span>
|
||||
{isDefault && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -2667,7 +2716,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search agents"
|
||||
placeholder={t('chat.modelControls.searchAgents')}
|
||||
value={agentSearchQuery}
|
||||
onChange={(e) => setAgentSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -2688,7 +2737,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RiArrowGoBackLine className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="font-medium">Reset to default</span>
|
||||
<span className="font-medium">{t('chat.modelControls.resetToDefault')}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
Reference in New Issue
Block a user