fix(web): split execution mode tabs and align active chip visuals (#418)

* Refine settings API configuration UX

* Fix PR validation issues

* Address settings review edge cases

* Address BYOK settings review feedback

* Fix BYOK provider labels and model display
This commit is contained in:
monshunter 2026-05-04 18:07:30 +08:00 committed by GitHub
parent 92087b515d
commit bf533c3d72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 998 additions and 246 deletions

View file

@ -2721,12 +2721,15 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
if (!['http:', 'https:'].includes(parsed.protocol)) {
return { error: 'Only http/https allowed' };
}
const hostname = parsed.hostname.toLowerCase();
const isLoopback =
['localhost', '127.0.0.1', '[::1]'].includes(hostname);
if (
['localhost', '127.0.0.1', '::1'].includes(parsed.hostname) ||
parsed.hostname.startsWith('169.254.') ||
parsed.hostname.startsWith('10.') ||
/^192\.168\./.test(parsed.hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(parsed.hostname)
!isLoopback &&
(hostname.startsWith('169.254.') ||
hostname.startsWith('10.') ||
/^192\.168\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname))
) {
return { error: 'Internal IPs blocked', forbidden: true };
}
@ -2755,10 +2758,10 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
const appendVersionedApiPath = (baseUrl, path) => {
const url = new URL(baseUrl);
url.pathname = url.pathname.replace(/\/+$/, '');
url.pathname = /\/v\d+$/.test(url.pathname)
? `${url.pathname}${path}`
: `${url.pathname}/v1${path}`;
const pathname = url.pathname.replace(/\/+$/, '');
url.pathname = /\/v\d+$/.test(pathname)
? `${pathname}${path}`
: `${pathname}/v1${path}`;
return url.toString();
};

View file

@ -59,6 +59,55 @@ describe('API proxy routes', () => {
);
});
it('allows loopback API base URLs for local OpenAI-compatible providers', async () => {
const fetchMock = vi.fn((input: FetchInput, init?: FetchInit) => {
const url = String(input);
if (url.startsWith(baseUrl)) return realFetch(input, init);
return Promise.resolve(sseResponse('data: [DONE]\n\n'));
});
vi.stubGlobal('fetch', fetchMock);
const res = await realFetch(`${baseUrl}/api/proxy/openai/stream`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
baseUrl: 'http://localhost:11434/v1',
apiKey: 'sk-local',
model: 'llama-local',
messages: [{ role: 'user', content: 'hello' }],
}),
});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('event: end');
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:11434/v1/chat/completions',
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer sk-local' }),
}),
);
});
it('blocks private network API base URLs before proxying', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
const res = await realFetch(`${baseUrl}/api/proxy/openai/stream`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
baseUrl: 'http://192.168.1.50:11434/v1',
apiKey: 'sk-private',
model: 'private-model',
messages: [{ role: 'user', content: 'hello' }],
}),
});
expect(res.status).toBe(403);
await expect(res.text()).resolves.toContain('Internal IPs blocked');
expect(fetchMock).not.toHaveBeenCalled();
});
it('surfaces OpenAI-compatible in-stream error frames', async () => {
vi.stubGlobal('fetch', vi.fn((input: FetchInput, init?: FetchInit) => {
const url = String(input);

View file

@ -284,10 +284,14 @@ export function App() {
[config],
);
const refreshAgents = useCallback(async () => {
const next = await fetchAgents();
setAgents(next);
}, []);
const refreshAgents = useCallback(
async (options?: { throwOnError?: boolean }) => {
const next = await fetchAgents(options);
setAgents(next);
return next;
},
[],
);
const handleCreateProject = useCallback(
async (input: CreateInput & { pendingPrompt?: string }) => {

View file

@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { assistantRoleLabel } from './AssistantMessage';
import type { ChatMessage } from '../types';
const t = () => 'Assistant';
describe('assistantRoleLabel', () => {
it('prefers the persisted assistant display name over the protocol id', () => {
const message: ChatMessage = {
id: 'message-1',
role: 'assistant',
content: '',
agentId: 'openai-api',
agentName: 'OpenAI API · google/gemma-4-e4b',
};
expect(assistantRoleLabel(message, t)).toBe('OpenAI API · google/gemma-4-e4b');
});
it('maps API protocol ids to readable labels when no display name is saved', () => {
const message: ChatMessage = {
id: 'message-2',
role: 'assistant',
content: '',
agentId: 'openai-api',
};
expect(assistantRoleLabel(message, t)).toBe('OpenAI API');
});
it('normalizes saved API protocol ids used as display names', () => {
const message: ChatMessage = {
id: 'message-3',
role: 'assistant',
content: '',
agentName: 'openai-api',
};
expect(assistantRoleLabel(message, t)).toBe('OpenAI API');
});
it('preserves an explicit local agent model in the display name', () => {
const message: ChatMessage = {
id: 'message-4',
role: 'assistant',
content: '',
agentId: 'claude',
agentName: 'Claude · claude-sonnet-4-6',
};
expect(assistantRoleLabel(message, t)).toBe('Claude · claude-sonnet-4-6');
});
it('adds the model reported by a local CLI initializing event', () => {
const message: ChatMessage = {
id: 'message-5',
role: 'assistant',
content: '',
agentId: 'claude',
agentName: 'Claude',
events: [{ kind: 'status', label: 'initializing', detail: 'claude-sonnet-4-6' }],
};
expect(assistantRoleLabel(message, t)).toBe('Claude · claude-sonnet-4-6');
});
});

View file

@ -8,7 +8,7 @@ import { Icon } from './Icon';
import { useT } from '../i18n';
import { unfinishedTodosFromEvents, type TodoItem } from '../runtime/todos';
import type { Dict } from '../i18n/types';
import { agentDisplayName } from '../utils/agentLabels';
import { agentDisplayName, exactAgentDisplayName } from '../utils/agentLabels';
import { exactDateTime, messageTime, relativeTimeLong } from '../utils/chatTime';
import type { AgentEvent, ChatMessage, ProjectFile } from '../types';
@ -150,13 +150,30 @@ function MessageTimestamp({ message, t }: { message: ChatMessage; t: TranslateFn
);
}
function assistantRoleLabel(message: ChatMessage, t: TranslateFn): string {
const fromMetadata = agentDisplayName(message.agentId, message.agentName);
if (fromMetadata) return fromMetadata;
export function assistantRoleLabel(message: ChatMessage, t: TranslateFn): string {
const model = assistantModelDetail(message);
const fromName = message.agentName?.trim();
if (fromName) return appendRoleModel(exactAgentDisplayName(fromName) ?? fromName, model);
const fromId = agentDisplayName(message.agentId);
if (fromId) return appendRoleModel(fromId, model);
const starting = message.events?.find(
(e) => e.kind === 'status' && e.label === 'starting' && e.detail,
) as Extract<AgentEvent, { kind: 'status' }> | undefined;
return agentDisplayName(starting?.detail) ?? t('assistant.role');
return appendRoleModel(agentDisplayName(starting?.detail) ?? t('assistant.role'), model);
}
function assistantModelDetail(message: ChatMessage): string | null {
const initializing = message.events?.find(
(e) => e.kind === 'status' && e.label === 'initializing' && e.detail,
) as Extract<AgentEvent, { kind: 'status' }> | undefined;
const detail = initializing?.detail?.trim();
if (!detail || detail === 'default') return null;
return detail;
}
function appendRoleModel(label: string, model: string | null): string {
if (!model || label.includes(' · ')) return label;
return `${label} · ${model}`;
}
function AssistantFooter({

View file

@ -27,6 +27,7 @@ import { NewProjectPanel, type CreateInput } from './NewProjectPanel';
import { PetRail } from './pet/PetRail';
import { PromptTemplatePreviewModal } from './PromptTemplatePreviewModal';
import { PromptTemplatesTab } from './PromptTemplatesTab';
import { apiProtocolLabel } from '../utils/apiProtocol';
type TopTab = 'designs' | 'examples' | 'design-systems' | 'image-templates' | 'video-templates';
@ -300,7 +301,7 @@ export function EntryView({
<span>
{config.mode === 'daemon'
? t('settings.localCli')
: t('settings.anthropicApi')}
: apiProtocolLabel(config.apiProtocol)}
</span>
<span style={{ color: 'var(--text-faint)' }}>·</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 180 }}>

View file

@ -21,8 +21,11 @@ import {
} from '../providers/registry';
import { composeSystemPrompt } from '@open-design/contracts';
import { navigate } from '../router';
import { agentDisplayName } from '../utils/agentLabels';
import { apiProtocolAgentId, apiProtocolLabel } from '../utils/apiProtocol';
import { agentDisplayName, agentModelDisplayName } from '../utils/agentLabels';
import {
apiProtocolAgentId,
apiProtocolModelLabel,
} from '../utils/apiProtocol';
import { playSound, showCompletionNotification } from '../utils/notifications';
import { DEFAULT_NOTIFICATIONS } from '../state/config';
import type { TodoItem } from '../runtime/todos';
@ -782,14 +785,22 @@ export function ProjectView({
config.mode === 'daemon' && config.agentId
? agentsById.get(config.agentId)
: null;
const selectedAgentChoice =
config.mode === 'daemon' && config.agentId
? config.agentModels?.[config.agentId]
: undefined;
const assistantAgentId =
config.mode === 'daemon'
? config.agentId ?? undefined
: apiProtocolAgentId(config.apiProtocol);
const assistantAgentName =
config.mode === 'daemon'
? assistantAgentDisplayName(config.agentId, selectedAgent?.name)
: apiProtocolLabel(config.apiProtocol);
? agentModelDisplayName(
config.agentId,
selectedAgent?.name,
selectedAgentChoice?.model,
)
: apiProtocolModelLabel(config.apiProtocol, config.model);
const assistantId = crypto.randomUUID();
const assistantMsg: ChatMessage = {
id: assistantId,
@ -1004,7 +1015,7 @@ export function ProjectView({
handlers.onError(new Error('Pick a local agent first (top bar).'));
return;
}
const choice = config.agentModels?.[config.agentId];
const choice = selectedAgentChoice;
void streamViaDaemon({
agentId: config.agentId,
history: nextHistory,

View file

@ -1,22 +1,10 @@
import { describe, expect, it } from 'vitest';
import { KNOWN_PROVIDERS } from '../state/config';
import type { ApiProtocol, AppConfig } from '../types';
function switchApiProtocol(config: AppConfig, protocol: ApiProtocol): AppConfig {
const currentProvider = config.apiProviderBaseUrl
? KNOWN_PROVIDERS.find((p) => p.baseUrl === config.apiProviderBaseUrl)
: undefined;
const stillOnSelectedProvider = Boolean(currentProvider && config.baseUrl === currentProvider.baseUrl);
const provider = KNOWN_PROVIDERS.find((p) => p.protocol === protocol);
return {
...config,
mode: 'api',
apiProtocol: protocol,
...(stillOnSelectedProvider && provider
? { baseUrl: provider.baseUrl, model: provider.model, apiProviderBaseUrl: provider.baseUrl }
: { apiProviderBaseUrl: null }),
};
}
import {
isValidApiBaseUrl,
switchApiProtocolConfig,
updateCurrentApiProtocolConfig,
} from './SettingsDialog';
import type { AppConfig } from '../types';
const baseConfig: AppConfig = {
mode: 'api',
@ -31,25 +19,71 @@ const baseConfig: AppConfig = {
};
describe('SettingsDialog API protocol switching', () => {
it('preserves custom baseUrl and model when switching protocol tabs', () => {
it('stores the current custom protocol config before loading another protocol', () => {
const config: AppConfig = {
...baseConfig,
apiKey: 'anthropic-key',
apiProviderBaseUrl: null,
baseUrl: 'https://my-proxy.example.com',
model: 'my-model',
};
expect(switchApiProtocol(config, 'openai')).toMatchObject({
const next = switchApiProtocolConfig(config, 'openai');
expect(next).toMatchObject({
mode: 'api',
apiProtocol: 'openai',
apiKey: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
});
expect(next.apiProtocolConfigs?.anthropic).toMatchObject({
apiKey: 'anthropic-key',
baseUrl: 'https://my-proxy.example.com',
model: 'my-model',
apiProviderBaseUrl: null,
});
});
it('auto-fills the new protocol default when switching from a selected known provider', () => {
expect(switchApiProtocol(baseConfig, 'openai')).toMatchObject({
it('restores each protocol draft instead of leaking shared field values', () => {
const openai = switchApiProtocolConfig(baseConfig, 'openai');
const openaiEdited = updateCurrentApiProtocolConfig(openai, {
apiKey: 'openai-key',
baseUrl: 'https://openai-proxy.example.com',
model: 'openai-model',
apiProviderBaseUrl: null,
});
const google = switchApiProtocolConfig(openaiEdited, 'google');
const googleEdited = updateCurrentApiProtocolConfig(google, {
apiKey: 'google-key',
baseUrl: 'https://google-proxy.example.com',
model: 'google-model',
apiProviderBaseUrl: null,
});
const restoredOpenai = switchApiProtocolConfig(googleEdited, 'openai');
expect(restoredOpenai).toMatchObject({
mode: 'api',
apiProtocol: 'openai',
apiKey: 'openai-key',
baseUrl: 'https://openai-proxy.example.com',
model: 'openai-model',
apiProviderBaseUrl: null,
});
expect(restoredOpenai.apiProtocolConfigs?.google).toMatchObject({
apiKey: 'google-key',
baseUrl: 'https://google-proxy.example.com',
model: 'google-model',
apiProviderBaseUrl: null,
});
});
it('loads the new protocol default on first visit', () => {
expect(switchApiProtocolConfig(baseConfig, 'openai')).toMatchObject({
mode: 'api',
apiProtocol: 'openai',
apiKey: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
@ -57,29 +91,56 @@ describe('SettingsDialog API protocol switching', () => {
});
it('auto-fills Google defaults when switching from a selected known provider', () => {
expect(switchApiProtocol(baseConfig, 'google')).toMatchObject({
expect(switchApiProtocolConfig(baseConfig, 'google')).toMatchObject({
mode: 'api',
apiProtocol: 'google',
apiKey: '',
baseUrl: 'https://generativelanguage.googleapis.com',
model: 'gemini-2.0-flash',
apiProviderBaseUrl: 'https://generativelanguage.googleapis.com',
});
});
it('preserves user-customized known-looking baseUrl when provider tracking was cleared', () => {
it('keeps Azure API version in the Azure draft only', () => {
const config: AppConfig = {
...baseConfig,
apiProviderBaseUrl: null,
baseUrl: 'https://api.openai.com/v1',
model: 'custom-openai-model',
apiProtocol: 'azure',
apiKey: 'azure-key',
model: 'deployment-one',
apiVersion: '2024-10-21',
};
expect(switchApiProtocol(config, 'anthropic')).toMatchObject({
mode: 'api',
apiProtocol: 'anthropic',
baseUrl: 'https://api.openai.com/v1',
model: 'custom-openai-model',
apiProviderBaseUrl: null,
const next = switchApiProtocolConfig(config, 'openai');
expect(next).toMatchObject({
apiProtocol: 'openai',
apiKey: '',
apiVersion: '',
});
expect(next.apiProtocolConfigs?.azure).toMatchObject({
apiKey: 'azure-key',
model: 'deployment-one',
apiVersion: '2024-10-21',
});
});
});
describe('SettingsDialog API Base URL validation', () => {
it('accepts public http/https URLs and loopback local providers', () => {
expect(isValidApiBaseUrl('https://api.openai.com/v1')).toBe(true);
expect(isValidApiBaseUrl('http://localhost:11434/v1')).toBe(true);
expect(isValidApiBaseUrl('http://127.0.0.1:11434/v1')).toBe(true);
expect(isValidApiBaseUrl('http://[::1]:11434/v1')).toBe(true);
expect(isValidApiBaseUrl(' https://resource.openai.azure.com ')).toBe(true);
expect(isValidApiBaseUrl('ddddd')).toBe(false);
expect(isValidApiBaseUrl('api.openai.com/v1')).toBe(false);
expect(isValidApiBaseUrl('ftp://api.example.com')).toBe(false);
expect(isValidApiBaseUrl('http:api.example.com')).toBe(false);
expect(isValidApiBaseUrl('https://')).toBe(false);
expect(isValidApiBaseUrl('http://10.0.0.5:11434/v1')).toBe(false);
expect(isValidApiBaseUrl('http://169.254.1.5:11434/v1')).toBe(false);
expect(isValidApiBaseUrl('http://172.16.0.5:11434/v1')).toBe(false);
expect(isValidApiBaseUrl('http://192.168.1.5:11434/v1')).toBe(false);
});
});

View file

@ -1,5 +1,5 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import type { CSSProperties, Dispatch, SetStateAction } from 'react';
import { LOCALE_LABEL, LOCALES, useI18n } from '../i18n';
import type { Locale } from '../i18n';
import { AgentIcon } from './AgentIcon';
@ -15,7 +15,7 @@ import {
MIN_MAX_TOKENS,
modelMaxTokensDefault,
} from '../state/maxTokens';
import type { AgentInfo, ApiProtocol, AppConfig, AppTheme, AppVersionInfo, ExecMode } from '../types';
import type { AgentInfo, ApiProtocol, ApiProtocolConfig, AppConfig, AppTheme, AppVersionInfo, ExecMode } from '../types';
import { MEDIA_PROVIDERS } from '../media/models';
import type { MediaProvider } from '../media/models';
import { PetSettings } from './pet/PetSettings';
@ -49,7 +49,9 @@ interface Props {
defaultSection?: SettingsSection;
onSave: (cfg: AppConfig) => void;
onClose: () => void;
onRefreshAgents: () => void;
onRefreshAgents: (
options?: { throwOnError?: boolean },
) => AgentInfo[] | Promise<AgentInfo[] | void> | void;
}
const SUGGESTED_MODELS_BY_PROTOCOL = {
@ -103,12 +105,11 @@ const SUGGESTED_MODELS_BY_PROTOCOL = {
const API_PROTOCOL_TABS: Array<{
id: ApiProtocol;
title: string;
meta: string;
}> = [
{ id: 'anthropic', title: 'Anthropic API', meta: '/v1/messages' },
{ id: 'openai', title: 'OpenAI API', meta: '/v1/chat/completions' },
{ id: 'azure', title: 'Azure OpenAI', meta: 'deployments/chat/completions' },
{ id: 'google', title: 'Google Gemini', meta: ':streamGenerateContent' },
{ id: 'anthropic', title: 'Anthropic' },
{ id: 'openai', title: 'OpenAI' },
{ id: 'azure', title: 'Azure OpenAI' },
{ id: 'google', title: 'Google Gemini' },
];
const API_PROTOCOL_LABELS: Record<ApiProtocol, string> = {
@ -125,6 +126,116 @@ const API_KEY_PLACEHOLDERS: Record<ApiProtocol, string> = {
google: 'AIza...',
};
type RescanNotice =
| { kind: 'success'; count: number }
| { kind: 'error' };
function defaultApiProtocolConfig(protocol: ApiProtocol): ApiProtocolConfig {
const provider = KNOWN_PROVIDERS.find((p) => p.protocol === protocol);
return {
apiKey: '',
baseUrl: provider?.baseUrl ?? '',
model: provider?.model ?? '',
apiVersion: '',
apiProviderBaseUrl: provider ? provider.baseUrl : null,
};
}
function currentApiProtocolConfig(config: AppConfig): ApiProtocolConfig {
return {
apiKey: config.apiKey,
baseUrl: config.baseUrl,
model: config.model,
apiVersion: config.apiVersion ?? '',
apiProviderBaseUrl: config.apiProviderBaseUrl ?? null,
};
}
function applyApiProtocolConfig(
config: AppConfig,
protocol: ApiProtocol,
apiConfig: ApiProtocolConfig,
): AppConfig {
return {
...config,
apiProtocol: protocol,
apiKey: apiConfig.apiKey,
baseUrl: apiConfig.baseUrl,
model: apiConfig.model,
apiProviderBaseUrl: apiConfig.apiProviderBaseUrl ?? null,
apiVersion: protocol === 'azure' ? (apiConfig.apiVersion ?? '') : '',
};
}
export function isValidApiBaseUrl(value: string): boolean {
const trimmed = value.trim();
if (!/^https?:\/\//i.test(trimmed)) return false;
try {
const url = new URL(trimmed);
const hostname = url.hostname.toLowerCase();
const isLoopback =
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '[::1]';
const isPrivateIpv4 =
hostname.startsWith('169.254.') ||
hostname.startsWith('10.') ||
/^192\.168\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
return (
(url.protocol === 'http:' || url.protocol === 'https:') &&
Boolean(url.hostname) &&
(isLoopback || !isPrivateIpv4)
);
} catch {
return false;
}
}
export function updateCurrentApiProtocolConfig(
config: AppConfig,
patch: Partial<ApiProtocolConfig>,
): AppConfig {
const protocol = config.apiProtocol ?? 'anthropic';
const nextApiConfig: ApiProtocolConfig = {
...currentApiProtocolConfig(config),
...patch,
};
return applyApiProtocolConfig(
{
...config,
apiProtocolConfigs: {
...(config.apiProtocolConfigs ?? {}),
[protocol]: nextApiConfig,
},
},
protocol,
nextApiConfig,
);
}
export function switchApiProtocolConfig(
config: AppConfig,
protocol: ApiProtocol,
): AppConfig {
const currentProtocol = config.apiProtocol ?? 'anthropic';
const apiProtocolConfigs = {
...(config.apiProtocolConfigs ?? {}),
[currentProtocol]: currentApiProtocolConfig(config),
};
const nextApiConfig =
apiProtocolConfigs[protocol] ?? defaultApiProtocolConfig(protocol);
return applyApiProtocolConfig(
{
...config,
mode: 'api',
apiProtocolConfigs,
},
protocol,
nextApiConfig,
);
}
export function SettingsDialog({
initial,
agents,
@ -158,6 +269,9 @@ export function SettingsDialog({
defaultSection ?? 'execution',
);
const [languageMenuRect, setLanguageMenuRect] = useState<DOMRect | null>(null);
const [agentRescanRunning, setAgentRescanRunning] = useState(false);
const [agentRescanNotice, setAgentRescanNotice] =
useState<RescanNotice | null>(null);
const languageRef = useRef<HTMLDivElement | null>(null);
// If the daemon goes offline mid-edit, force API mode so the UI doesn't
@ -209,35 +323,50 @@ export function SettingsDialog({
);
const setMode = (mode: ExecMode) => setCfg((c) => ({ ...c, mode }));
const setApiProtocol = (protocol: ApiProtocol) => {
setCfg((c) => {
const currentProvider = c.apiProviderBaseUrl
? KNOWN_PROVIDERS.find((p) => p.baseUrl === c.apiProviderBaseUrl)
: undefined;
const stillOnSelectedProvider = Boolean(currentProvider && c.baseUrl === currentProvider.baseUrl);
const provider = KNOWN_PROVIDERS.find((p) => p.protocol === protocol);
return {
...c,
mode: 'api',
apiProtocol: protocol,
...(stillOnSelectedProvider && provider
? { baseUrl: provider.baseUrl, model: provider.model, apiProviderBaseUrl: provider.baseUrl }
: { apiProviderBaseUrl: null }),
};
});
const setApiProtocol = (protocol: ApiProtocol) =>
setCfg((c) => switchApiProtocolConfig(c, protocol));
const updateApiConfig = (patch: Partial<ApiProtocolConfig>) =>
setCfg((c) => updateCurrentApiProtocolConfig(c, patch));
const handleRefreshAgents = async () => {
if (agentRescanRunning) return;
setAgentRescanRunning(true);
setAgentRescanNotice(null);
try {
const refreshed = await onRefreshAgents({ throwOnError: true });
const nextAgents = Array.isArray(refreshed) ? refreshed : agents;
setAgentRescanNotice({
kind: 'success',
count: nextAgents.filter((a) => a.available).length,
});
} catch {
setAgentRescanNotice({ kind: 'error' });
} finally {
setAgentRescanRunning(false);
}
};
const apiProtocol = cfg.apiProtocol ?? 'anthropic';
const baseUrlValid = isValidApiBaseUrl(cfg.baseUrl);
const baseUrlInvalid = Boolean(cfg.baseUrl.trim() && !baseUrlValid);
const canSave =
cfg.mode === 'daemon'
? Boolean(cfg.agentId && agents.find((a) => a.id === cfg.agentId)?.available)
: Boolean(cfg.apiKey.trim() && cfg.model.trim() && cfg.baseUrl.trim());
: Boolean(
cfg.apiKey.trim() &&
cfg.model.trim() &&
baseUrlValid,
);
const apiProtocol = cfg.apiProtocol ?? 'anthropic';
const protocolProviders = useMemo(
() => KNOWN_PROVIDERS.filter((p) => p.protocol === apiProtocol),
[apiProtocol],
);
const selectedProviderIndex = protocolProviders.findIndex((p) => p.baseUrl === cfg.baseUrl);
const selectedProviderIndex =
cfg.apiProviderBaseUrl == null
? -1
: protocolProviders.findIndex(
(p) => p.baseUrl === cfg.apiProviderBaseUrl && p.baseUrl === cfg.baseUrl,
);
const selectedProvider = selectedProviderIndex >= 0 ? protocolProviders[selectedProviderIndex] : undefined;
const apiModelOptions = useMemo(
() => Array.from(new Set(
@ -304,7 +433,7 @@ export function SettingsDialog({
<Icon name="sliders" size={18} />
<span>
<strong>{t('settings.envConfigure')}</strong>
<small>{t('settings.codeAgent')}</small>
<small>{`${t('settings.localCli')} / ${t('settings.modeApiMeta')}`}</small>
</span>
</button>
<button
@ -381,7 +510,7 @@ export function SettingsDialog({
className="seg-control"
role="tablist"
aria-label={t('settings.modeAria')}
style={{ gridTemplateColumns: `repeat(${API_PROTOCOL_TABS.length + 1}, 1fr)` }}
style={{ ['--seg-cols' as string]: 2 } as CSSProperties}
>
<button
type="button"
@ -396,43 +525,87 @@ export function SettingsDialog({
: t('settings.modeDaemonOffline')
}
>
<span className="seg-title">{t('settings.modeDaemon')}</span>
<span className="seg-title">{t('settings.localCli')}</span>
<span className="seg-meta">
{daemonLive
? t('settings.modeDaemonInstalledMeta', { count: installedCount })
: t('settings.modeDaemonOfflineMeta')}
</span>
</button>
{API_PROTOCOL_TABS.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={cfg.mode === 'api' && apiProtocol === tab.id}
className={'seg-btn' + (cfg.mode === 'api' && apiProtocol === tab.id ? ' active' : '')}
onClick={() => setApiProtocol(tab.id)}
>
<span className="seg-title">{tab.title}</span>
<span className="seg-meta">{tab.meta}</span>
</button>
))}
<button
type="button"
role="tab"
aria-selected={cfg.mode === 'api'}
className={'seg-btn' + (cfg.mode === 'api' ? ' active' : '')}
onClick={() => setMode('api')}
>
<span className="seg-title">{t('settings.modeApiMeta')}</span>
<span className="seg-meta">{t('settings.modeApi')}</span>
</button>
</div>
{cfg.mode === 'api' ? (
<div
className="protocol-chips"
role="tablist"
aria-label={t('settings.protocolAria')}
>
{API_PROTOCOL_TABS.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={apiProtocol === tab.id}
className={'protocol-chip' + (apiProtocol === tab.id ? ' active' : '')}
onClick={() => setApiProtocol(tab.id)}
>
{tab.title}
</button>
))}
</div>
) : null}
{cfg.mode === 'daemon' ? (
<section className="settings-section">
<div className="section-head">
<div>
<h3>{t('settings.codeAgent')}</h3>
<h3>{t('settings.localCli')}</h3>
<p className="hint">{t('settings.codeAgentHint')}</p>
</div>
<button
type="button"
className="ghost icon-btn"
onClick={onRefreshAgents}
className={
'ghost icon-btn settings-rescan-btn' +
(agentRescanRunning ? ' loading' : '')
}
onClick={() => void handleRefreshAgents()}
disabled={agentRescanRunning}
title={t('settings.rescanTitle')}
>
{t('settings.rescan')}
{agentRescanRunning ? (
<>
<Icon name="spinner" size={13} className="icon-spin" />
<span>{t('settings.rescanRunning')}</span>
</>
) : (
t('settings.rescan')
)}
</button>
</div>
{agentRescanNotice ? (
<p
className={
'settings-rescan-status ' + agentRescanNotice.kind
}
role={
agentRescanNotice.kind === 'error' ? 'alert' : 'status'
}
>
{agentRescanNotice.kind === 'success'
? t('settings.rescanSuccess', {
count: agentRescanNotice.count,
})
: t('settings.rescanFailed')}
</p>
) : null}
{agents.length === 0 ? (
<div className="empty-card">
{t('settings.noAgentsDetected')}
@ -592,31 +765,35 @@ export function SettingsDialog({
) : (
<section className="settings-section">
<div className="section-head">
<h3>{API_PROTOCOL_LABELS[apiProtocol]}</h3>
<div>
<h3>{API_PROTOCOL_LABELS[apiProtocol]}</h3>
</div>
</div>
<label className="field">
<span className="field-label">Quick fill provider</span>
<span className="field-label">{t('settings.quickFillProvider')}</span>
<select
value={selectedProviderIndex >= 0 ? String(selectedProviderIndex) : ''}
onChange={(e) => {
if (e.target.value === '') {
setCfg((c) => ({ ...c, baseUrl: '', model: '', apiProviderBaseUrl: null }));
updateApiConfig({
baseUrl: '',
model: '',
apiProviderBaseUrl: null,
});
return;
}
const idx = Number(e.target.value);
if (!isNaN(idx) && protocolProviders[idx]) {
const p = protocolProviders[idx]!;
setCfg((c) => ({
...c,
apiProtocol: p.protocol,
updateApiConfig({
baseUrl: p.baseUrl,
model: p.model,
apiProviderBaseUrl: p.baseUrl,
}));
});
}
}}
>
<option value="">Custom provider</option>
<option value="">{t('settings.customProvider')}</option>
{protocolProviders.map((p, i) => (
<option key={p.label} value={i}>{p.label}</option>
))}
@ -629,7 +806,7 @@ export function SettingsDialog({
type={showApiKey ? 'text' : 'password'}
placeholder={API_KEY_PLACEHOLDERS[apiProtocol]}
value={cfg.apiKey}
onChange={(e) => setCfg({ ...cfg, apiKey: e.target.value })}
onChange={(e) => updateApiConfig({ apiKey: e.target.value })}
autoFocus
/>
<button
@ -645,14 +822,18 @@ export function SettingsDialog({
</div>
</label>
<label className="field">
<span className="field-label">{t('settings.model')}</span>
<span className="field-label">
{apiProtocol === 'azure'
? t('settings.azureDeploymentModel')
: t('settings.model')}
</span>
<select
value={apiModelSelectValue}
onChange={(e) => {
if (e.target.value === CUSTOM_MODEL_SENTINEL) {
setCfg((c) => ({ ...c, model: '' }));
updateApiConfig({ model: '' });
} else {
setCfg((c) => ({ ...c, model: e.target.value }));
updateApiConfig({ model: e.target.value });
}
}}
>
@ -663,7 +844,10 @@ export function SettingsDialog({
</select>
</label>
{!selectedProvider ? (
<p className="hint">These are suggested models for this protocol. Your provider may support different models.</p>
<p className="hint">{t('settings.suggestedModelsHint')}</p>
) : null}
{apiProtocol === 'azure' ? (
<p className="hint">{t('settings.azureDeploymentModelHint')}</p>
) : null}
{apiModelCustom || apiModelSelectValue === CUSTOM_MODEL_SENTINEL ? (
<label className="field">
@ -672,26 +856,40 @@ export function SettingsDialog({
type="text"
value={cfg.model}
placeholder={t('settings.modelCustomPlaceholder')}
onChange={(e) => setCfg({ ...cfg, model: e.target.value.trim() })}
onChange={(e) => updateApiConfig({ model: e.target.value.trim() })}
/>
</label>
) : null}
<label className="field">
<span className="field-label">{t('settings.baseUrl')}</span>
<input
type="text"
type="url"
inputMode="url"
value={cfg.baseUrl}
onChange={(e) => setCfg({ ...cfg, baseUrl: e.target.value, apiProviderBaseUrl: null })}
aria-invalid={baseUrlInvalid || undefined}
aria-describedby={
baseUrlInvalid ? 'settings-base-url-error' : undefined
}
onChange={(e) => updateApiConfig({ baseUrl: e.target.value, apiProviderBaseUrl: null })}
/>
{baseUrlInvalid ? (
<span
id="settings-base-url-error"
className="settings-field-error"
role="alert"
>
{t('settings.baseUrlInvalid')}
</span>
) : null}
</label>
{apiProtocol === 'azure' ? (
<label className="field">
<span className="field-label">API version</span>
<span className="field-label">{t('settings.apiVersion')}</span>
<input
type="text"
value={cfg.apiVersion ?? ''}
placeholder="2024-10-21"
onChange={(e) => setCfg({ ...cfg, apiVersion: e.target.value.trim() })}
onChange={(e) => updateApiConfig({ apiVersion: e.target.value.trim() })}
/>
</label>
) : null}

View file

@ -48,36 +48,47 @@ export const ar: Dict = {
"اختر كيف تريد تشغيل الأجيال. يمكنك تغيير هذا في أي وقت من زر الإعدادات في الشريط العلوي.",
'settings.kicker': 'الإعدادات',
'settings.title': 'التنفيذ والنموذج',
'settings.subtitle':
'اختر بين واجهة CLI محلية و Anthropic API (BYOK). مفتاح API الخاص بك مخزن فقط في هذا المتصفح.',
'settings.subtitle': 'اختر بين CLI المحلي و BYOK. يتم حفظ مفتاح API في هذا المتصفح فقط.',
'settings.modeAria': 'وضع التنفيذ',
'settings.protocolAria': 'بروتوكول API',
'settings.modeDaemon': 'CLI محلي',
'settings.modeDaemonHelp': 'التشغيل عبر واجهة CLI على جهازك',
'settings.modeDaemonOffline': 'البرنامج الخفي لا يعمل',
'settings.modeDaemonOfflineMeta': 'غير متصل',
'settings.modeDaemonInstalledMeta': '{count} مثبت',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'مزود API',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'وكيل الكود',
'settings.codeAgentHint':
'تم اكتشافه عبر مسح PATH الخاص بك. اختر واجهة CLI التي تريد أن تمر الأجيال عبرها.',
'settings.rescan': '↻ إعادة المسح',
'settings.rescanTitle': 'إعادة مسح PATH',
'settings.rescanRunning': 'جارٍ المسح...',
'settings.rescanSuccess': 'اكتمل المسح. {count} متاح.',
'settings.rescanFailed': 'فشل المسح. تحقق من البرنامج الخفي وحاول مرة أخرى.',
'settings.noAgentsDetected':
'لم يتم اكتشاف أي وكلاء بعد. قم بتثبيت Claude Code أو Codex أو Devin أو Gemini CLI أو OpenCode أو Cursor Agent أو Qwen أو GitHub Copilot CLI، ثم اضغط على إعادة المسح.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'ملء المزوّد سريعًا',
'settings.customProvider': 'مزوّد مخصص',
'settings.apiKey': 'مفتاح API',
'settings.showKey': 'إظهار المفتاح',
'settings.hideKey': 'إخفاء المفتاح',
'settings.show': 'إظهار',
'settings.hide': 'إخفاء',
'settings.model': 'النموذج',
'settings.suggestedModelsHint':
'هذه نماذج مقترحة لهذا البروتوكول. قد يدعم مزوّدك نماذج مختلفة.',
'settings.baseUrl': 'رابط القاعدة',
'settings.baseUrlInvalid': 'أدخل رابط http:// أو https:// عام وصالح. يُسمح بـ localhost؛ ويتم حظر عناوين IP للشبكات الخاصة.',
'settings.azureDeploymentModel': 'اسم النشر',
'settings.azureDeploymentModelHint':
'في Azure OpenAI، يُستخدم هذا الحقل كاسم النشر في /openai/deployments/<model>. أدخل اسم النشر الذي أنشأته في Azure.',
'settings.apiVersion': 'إصدار API',
'settings.maxTokens': 'أقصى عدد من الرموز (اختياري)',
'settings.maxTokensHint':
'الحد الأقصى لطول الاستجابة. لكل نموذج قيمة افتراضية؛ اتركها فارغة لاستخدامها، أو أدخل رقماً للتجاوز.',
'settings.apiHint':
'المكالمات تذهب مباشرة من هذا المتصفح إلى رابط القاعدة الذي حددته. لا يوجد بروكسي. المفتاح لا يغادر localStorage.',
'settings.apiHint': 'تُرسل الطلبات عبر وكيل daemon المحلي إلى Base URL الذي تحدده. يُحفظ المفتاح في هذا المتصفح فقط ويُرسل مع طلبات المزود.',
'settings.skipForNow': 'تخطي الآن',
'settings.getStarted': 'ابدأ الآن',
'settings.envConfigure': 'تكوين وضع التنفيذ',
@ -319,7 +330,7 @@ export const ar: Dict = {
'avatar.localCli': 'CLI محلي',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'استخدام CLI محلي',
'avatar.useApi': 'استخدام Anthropic API',
'avatar.useApi': 'استخدام API · BYOK',
'avatar.codeAgent': 'وكيل الكود',
'avatar.rescan': 'إعادة مسح PATH',
'avatar.settings': 'الإعدادات',
@ -595,7 +606,7 @@ export const ar: Dict = {
'agentPicker.modeChoose': 'اختر وضع التنفيذ',
'agentPicker.localCli': 'CLI محلي',
'agentPicker.daemonOff': 'البرنامج الخفي متوقف',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'اختر وكيل كود مكتشف',
'agentPicker.noAgents': 'لا يوجد وكلاء في PATH',
'agentPicker.notInstalled': 'غير مثبت',

View file

@ -48,36 +48,47 @@ export const de: Dict = {
'Wählen Sie aus, wie Generierungen ausgeführt werden sollen. Sie können dies jederzeit über die Schaltfläche „Einstellungen“ in der oberen Leiste ändern.',
'settings.kicker': 'Einstellungen',
'settings.title': 'Ausführung & Modell',
'settings.subtitle':
'Wählen Sie zwischen einer lokalen Code-Agent-CLI und der Anthropic API (BYOK). Ihr API-Key wird nur in diesem Browser gespeichert.',
'settings.subtitle': 'Wählen Sie zwischen lokaler CLI und BYOK. Ihr API-Schlüssel wird nur in diesem Browser gespeichert.',
'settings.modeAria': 'Ausführungsmodus',
'settings.protocolAria': 'API-Protokoll',
'settings.modeDaemon': 'Lokale CLI',
'settings.modeDaemonHelp': 'Über eine Code-Agent-CLI auf Ihrem Rechner ausführen',
'settings.modeDaemonOffline': 'Daemon läuft nicht',
'settings.modeDaemonOfflineMeta': 'Daemon offline',
'settings.modeDaemonInstalledMeta': '{count} installiert',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API-Anbieter',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Code-Agent',
'settings.codeAgentHint':
'Durch Scannen Ihres PATH erkannt. Wählen Sie die CLI, über die Generierungen laufen sollen.',
'settings.rescan': '↻ Neu scannen',
'settings.rescanTitle': 'PATH erneut scannen',
'settings.rescanRunning': 'Scannen...',
'settings.rescanSuccess': 'Scan abgeschlossen. {count} verfuegbar.',
'settings.rescanFailed': 'Scan fehlgeschlagen. Pruefen Sie den Daemon und versuchen Sie es erneut.',
'settings.noAgentsDetected':
'Noch keine Agents erkannt. Installieren Sie Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen oder GitHub Copilot CLI und klicken Sie dann auf Neu scannen.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Anbieter schnell ausfüllen',
'settings.customProvider': 'Benutzerdefinierter Anbieter',
'settings.apiKey': 'API-Key',
'settings.showKey': 'Key anzeigen',
'settings.hideKey': 'Key ausblenden',
'settings.show': 'Anzeigen',
'settings.hide': 'Ausblenden',
'settings.model': 'Modell',
'settings.suggestedModelsHint':
'Dies sind vorgeschlagene Modelle für dieses Protokoll. Ihr Anbieter kann andere Modelle unterstützen.',
'settings.baseUrl': 'Base URL',
'settings.baseUrlInvalid': 'Geben Sie eine gültige öffentliche http://- oder https://-URL ein. Localhost ist erlaubt; private Netzwerk-IPs werden blockiert.',
'settings.azureDeploymentModel': 'Deployment-Name',
'settings.azureDeploymentModelHint':
'Fuer Azure OpenAI wird dieses Feld als Deployment-Name in /openai/deployments/<model> verwendet. Geben Sie den in Azure angelegten Deployment-Namen ein.',
'settings.apiVersion': 'API-Version',
'settings.maxTokens': 'Max. Tokens (optional)',
'settings.maxTokensHint':
'Obergrenze für die Antwortlänge. Jedes Modell hat einen abgestimmten Standardwert (im Platzhalter sichtbar); leer lassen, um ihn zu verwenden, oder eine Zahl eingeben, um ihn zu überschreiben.',
'settings.apiHint':
'Aufrufe gehen direkt von diesem Browser an die festgelegte Base URL. Kein Proxy. Der Key verlässt localStorage nie.',
'settings.apiHint': 'Anfragen werden über den lokalen Daemon-Proxy an die festgelegte Base URL gesendet. Der Schlüssel wird nur in diesem Browser gespeichert und mit Provider-Anfragen gesendet.',
'settings.skipForNow': 'Vorerst überspringen',
'settings.getStarted': 'Loslegen',
'settings.envConfigure': 'Ausführungsmodus konfigurieren',
@ -319,7 +330,7 @@ export const de: Dict = {
'avatar.localCli': 'Lokale CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Lokale CLI verwenden',
'avatar.useApi': 'Anthropic API verwenden',
'avatar.useApi': 'API · BYOK verwenden',
'avatar.codeAgent': 'Code-Agent',
'avatar.rescan': 'PATH neu scannen',
'avatar.settings': 'Einstellungen',
@ -595,7 +606,7 @@ export const de: Dict = {
'agentPicker.modeChoose': 'Ausführungsmodus wählen',
'agentPicker.localCli': 'Lokale CLI',
'agentPicker.daemonOff': 'Daemon aus',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Erkannte Code-Agent-CLI auswählen',
'agentPicker.noAgents': 'keine Agents im PATH',
'agentPicker.notInstalled': 'nicht installiert',

View file

@ -48,36 +48,47 @@ export const en: Dict = {
"Pick how you'd like to run generations. You can change this any time from the Settings button in the top bar.",
'settings.kicker': 'Settings',
'settings.title': 'Execution & model',
'settings.subtitle':
'Choose between a local code-agent CLI and the Anthropic API (BYOK). Your API key is stored only in this browser.',
'settings.subtitle': 'Choose between Local CLI and BYOK. Your API key is stored only in this browser.',
'settings.modeAria': 'Execution mode',
'settings.protocolAria': 'API protocol',
'settings.modeDaemon': 'Local CLI',
'settings.modeDaemonHelp': 'Run via a code-agent CLI on your machine',
'settings.modeDaemonOffline': 'Daemon is not running',
'settings.modeDaemonOfflineMeta': 'daemon offline',
'settings.modeDaemonInstalledMeta': '{count} installed',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API provider',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Code agent',
'settings.codeAgentHint':
'Detected by scanning your PATH. Pick the CLI you want generations to flow through.',
'settings.rescan': '↻ Rescan',
'settings.rescanTitle': 'Re-scan PATH',
'settings.rescanRunning': 'Scanning...',
'settings.rescanSuccess': 'Scan complete. {count} available.',
'settings.rescanFailed': 'Scan failed. Check the daemon and try again.',
'settings.noAgentsDetected':
'No agents detected yet. Install one of Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen, or GitHub Copilot CLI, then click Rescan.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Quick fill provider',
'settings.customProvider': 'Custom provider',
'settings.apiKey': 'API key',
'settings.showKey': 'Show key',
'settings.hideKey': 'Hide key',
'settings.show': 'Show',
'settings.hide': 'Hide',
'settings.model': 'Model',
'settings.suggestedModelsHint':
'These are suggested models for this protocol. Your provider may support different models.',
'settings.baseUrl': 'Base URL',
'settings.baseUrlInvalid': 'Enter a valid public http:// or https:// URL. Localhost is allowed; private network IPs are blocked.',
'settings.azureDeploymentModel': 'Deployment name',
'settings.azureDeploymentModelHint':
'For Azure OpenAI, this field is used as the deployment name in /openai/deployments/<model>. Enter the deployment name you created in Azure.',
'settings.apiVersion': 'API version',
'settings.maxTokens': 'Max tokens (optional)',
'settings.maxTokensHint':
'Cap on the response length. Each model has a tuned default (shown as a placeholder); leave blank to use it, or enter a number to override.',
'settings.apiHint':
'Calls go directly from this browser to the base URL you set. No proxy. The key never leaves localStorage.',
'settings.apiHint': 'Calls are sent through the local daemon proxy to the base URL you set. The key is stored only in this browser and sent with provider requests.',
'settings.skipForNow': 'Skip for now',
'settings.getStarted': 'Get started',
'settings.envConfigure': 'Configure execution mode',
@ -319,7 +330,7 @@ export const en: Dict = {
'avatar.localCli': 'Local CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Use Local CLI',
'avatar.useApi': 'Use Anthropic API',
'avatar.useApi': 'Use API · BYOK',
'avatar.codeAgent': 'Code agent',
'avatar.rescan': 'Rescan PATH',
'avatar.settings': 'Settings',
@ -595,7 +606,7 @@ export const en: Dict = {
'agentPicker.modeChoose': 'Choose execution mode',
'agentPicker.localCli': 'Local CLI',
'agentPicker.daemonOff': 'daemon off',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Select a detected code-agent CLI',
'agentPicker.noAgents': 'no agents on PATH',
'agentPicker.notInstalled': 'not installed',

View file

@ -48,36 +48,47 @@ export const esES: Dict = {
'Elige cómo quieres ejecutar las generaciones. Puedes cambiarlo en cualquier momento desde el botón Ajustes en la barra superior.',
'settings.kicker': 'Ajustes',
'settings.title': 'Ejecución y modelo',
'settings.subtitle':
'Elige entre una CLI local de agente de código y la API de Anthropic (BYOK). Tu clave de API se guarda solo en este navegador.',
'settings.subtitle': 'Elige entre CLI local y BYOK. Tu clave de API se guarda solo en este navegador.',
'settings.modeAria': 'Modo de ejecución',
'settings.protocolAria': 'Protocolo de API',
'settings.modeDaemon': 'CLI local',
'settings.modeDaemonHelp': 'Ejecuta a través de una CLI de agente de código en tu máquina',
'settings.modeDaemonOffline': 'El daemon no está en ejecución',
'settings.modeDaemonOfflineMeta': 'daemon sin conexión',
'settings.modeDaemonInstalledMeta': '{count} instalados',
'settings.modeApi': 'API de Anthropic',
'settings.modeApi': 'Proveedor de API',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Agente de código',
'settings.codeAgentHint':
'Detectado al escanear tu PATH. Elige la CLI por la que quieres que pasen las generaciones.',
'settings.rescan': '↻ Reescanear',
'settings.rescanTitle': 'Reescanear PATH',
'settings.rescanRunning': 'Escaneando...',
'settings.rescanSuccess': 'Escaneo completado. {count} disponibles.',
'settings.rescanFailed': 'El escaneo falló. Comprueba el daemon e inténtalo de nuevo.',
'settings.noAgentsDetected':
'Aún no se ha detectado ningún agente. Instala Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen o GitHub Copilot CLI y pulsa Reescanear.',
'settings.apiSection': 'API de Anthropic',
'settings.quickFillProvider': 'Rellenar proveedor',
'settings.customProvider': 'Proveedor personalizado',
'settings.apiKey': 'Clave de API',
'settings.showKey': 'Mostrar clave',
'settings.hideKey': 'Ocultar clave',
'settings.show': 'Mostrar',
'settings.hide': 'Ocultar',
'settings.model': 'Modelo',
'settings.suggestedModelsHint':
'Estos son modelos sugeridos para este protocolo. Tu proveedor puede admitir modelos diferentes.',
'settings.baseUrl': 'URL base',
'settings.baseUrlInvalid': 'Introduce una URL pública http:// o https:// válida. Localhost está permitido; las IPs de red privada se bloquean.',
'settings.azureDeploymentModel': 'Nombre del despliegue',
'settings.azureDeploymentModelHint':
'Para Azure OpenAI, este campo se usa como nombre del despliegue en /openai/deployments/<model>. Introduce el nombre del despliegue que creaste en Azure.',
'settings.apiVersion': 'Versión de API',
'settings.maxTokens': 'Tokens máx. (opcional)',
'settings.maxTokensHint':
'Tope para la longitud de la respuesta. Cada modelo tiene un valor por defecto ajustado (visible en el placeholder); déjalo vacío para usarlo o introduce un número para anularlo.',
'settings.apiHint':
'Las llamadas van directamente desde este navegador a la URL base que indiques. Sin proxy. La clave nunca sale de localStorage.',
'settings.apiHint': 'Las llamadas pasan por el proxy del daemon local hasta la URL base configurada. La clave se guarda solo en este navegador y se envía con las solicitudes al proveedor.',
'settings.skipForNow': 'Omitir por ahora',
'settings.getStarted': 'Empezar',
'settings.envConfigure': 'Configurar el modo de ejecución',
@ -320,7 +331,7 @@ export const esES: Dict = {
'avatar.localCli': 'CLI local',
'avatar.anthropicApi': 'API de Anthropic',
'avatar.useLocal': 'Usar CLI local',
'avatar.useApi': 'Usar API de Anthropic',
'avatar.useApi': 'Usar API · BYOK',
'avatar.codeAgent': 'Agente de código',
'avatar.rescan': 'Reescanear PATH',
'avatar.settings': 'Ajustes',
@ -596,7 +607,7 @@ export const esES: Dict = {
'agentPicker.modeChoose': 'Elige el modo de ejecución',
'agentPicker.localCli': 'CLI local',
'agentPicker.daemonOff': 'daemon apagado',
'agentPicker.byok': 'API de Anthropic · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Selecciona una CLI de agente de código detectada',
'agentPicker.noAgents': 'no hay agentes en el PATH',
'agentPicker.notInstalled': 'no instalado',

View file

@ -48,36 +48,47 @@ export const fa: Dict = {
'نحوه اجرای تولیدات را انتخاب کنید. می‌توانید هر زمان از دکمه تنظیمات در نوار بالا این را تغییر دهید.',
'settings.kicker': 'تنظیمات',
'settings.title': 'اجرا و مدل',
'settings.subtitle':
'بین یک CLI عامل کد محلی و Anthropic API (BYOK) انتخاب کنید. کلید API شما فقط در این مرورگر ذخیره می‌شود.',
'settings.subtitle': 'بین CLI محلی و BYOK انتخاب کنید. کلید API فقط در همین مرورگر ذخیره می‌شود.',
'settings.modeAria': 'حالت اجرا',
'settings.protocolAria': 'پروتکل API',
'settings.modeDaemon': 'CLI محلی',
'settings.modeDaemonHelp': 'اجرا از طریق CLI عامل کد روی دستگاه شما',
'settings.modeDaemonOffline': 'Daemon در حال اجرا نیست',
'settings.modeDaemonOfflineMeta': 'daemon آفلاین',
'settings.modeDaemonInstalledMeta': '{count} نصب شده',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'ارائه‌دهنده API',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'عامل کد',
'settings.codeAgentHint':
'با اسکن PATH شما شناسایی شده. CLI مورد نظر برای جریان تولیدات را انتخاب کنید.',
'settings.rescan': '↻ اسکن مجدد',
'settings.rescanTitle': 'اسکن مجدد PATH',
'settings.rescanRunning': 'در حال اسکن...',
'settings.rescanSuccess': 'اسکن کامل شد. {count} مورد در دسترس است.',
'settings.rescanFailed': 'اسکن ناموفق بود. daemon را بررسی کنید و دوباره تلاش کنید.',
'settings.noAgentsDetected':
'هنوز هیچ عاملی شناسایی نشده. یکی از Claude Code، Codex، Gemini CLI، OpenCode، Cursor Agent، Qwen یا GitHub Copilot CLI را نصب کنید، سپس روی اسکن مجدد کلیک کنید.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'پر کردن سریع ارائه‌دهنده',
'settings.customProvider': 'ارائه‌دهنده سفارشی',
'settings.apiKey': 'کلید API',
'settings.showKey': 'نمایش کلید',
'settings.hideKey': 'پنهان کردن کلید',
'settings.show': 'نمایش',
'settings.hide': 'پنهان',
'settings.model': 'مدل',
'settings.suggestedModelsHint':
'این‌ها مدل‌های پیشنهادی برای این پروتکل هستند. ارائه‌دهنده شما ممکن است مدل‌های دیگری را پشتیبانی کند.',
'settings.baseUrl': 'آدرس پایه',
'settings.baseUrlInvalid': 'یک URL عمومی معتبر با http:// یا https:// وارد کنید. localhost مجاز است؛ IPهای شبکه خصوصی مسدود می‌شوند.',
'settings.azureDeploymentModel': 'نام استقرار',
'settings.azureDeploymentModelHint':
'در Azure OpenAI، این فیلد به عنوان نام استقرار در /openai/deployments/<model> استفاده می‌شود. نام استقراری را که در Azure ساخته‌اید وارد کنید.',
'settings.apiVersion': 'نسخه API',
'settings.maxTokens': 'حداکثر توکن (اختیاری)',
'settings.maxTokensHint':
'سقف طول پاسخ. هر مدل مقدار پیش‌فرض تنظیم‌شدهٔ خود را دارد (در placeholder نمایش داده می‌شود)؛ برای استفاده از آن خالی بگذارید، یا برای جایگزینی، عددی وارد کنید.',
'settings.apiHint':
'فراخوانی‌ها مستقیماً از این مرورگر به آدرس پایه‌ای که تعیین کرده‌اید ارسال می‌شوند. بدون پراکسی. کلید هرگز localStorage را ترک نمی‌کند.',
'settings.apiHint': 'درخواست‌ها از طریق پراکسی daemon محلی به Base URL تنظیم‌شده ارسال می‌شوند. کلید فقط در همین مرورگر ذخیره می‌شود و همراه درخواست‌های ارائه‌دهنده فرستاده می‌شود.',
'settings.skipForNow': 'فعلاً رد کنید',
'settings.getStarted': 'شروع کنید',
'settings.envConfigure': 'پیکربندی حالت اجرا',
@ -319,7 +330,7 @@ export const fa: Dict = {
'avatar.localCli': 'CLI محلی',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'استفاده از CLI محلی',
'avatar.useApi': 'استفاده از Anthropic API',
'avatar.useApi': 'استفاده از API · BYOK',
'avatar.codeAgent': 'عامل کد',
'avatar.rescan': 'اسکن مجدد PATH',
'avatar.settings': 'تنظیمات',
@ -566,7 +577,7 @@ export const fa: Dict = {
'agentPicker.modeChoose': 'انتخاب حالت اجرا',
'agentPicker.localCli': 'CLI محلی',
'agentPicker.daemonOff': 'daemon خاموش',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'انتخاب یک CLI عامل کد شناسایی شده',
'agentPicker.noAgents': 'هیچ عاملی در PATH وجود ندارد',
'agentPicker.notInstalled': 'نصب نشده',

View file

@ -48,36 +48,47 @@ export const fr: Dict = {
'Choisissez comment vous souhaitez exécuter les générations. Vous pouvez changer cela à tout moment depuis le bouton Paramètres dans la barre supérieure.',
'settings.kicker': 'Paramètres',
'settings.title': 'Exécution et modèle',
'settings.subtitle':
'Choisissez entre un agent CLI local et l\'API Anthropic (BYOK). Votre clé API est stockée uniquement dans ce navigateur.',
'settings.subtitle': 'Choisissez entre CLI local et BYOK. Votre clé API est stockée uniquement dans ce navigateur.',
'settings.modeAria': 'Mode d\'exécution',
'settings.protocolAria': 'Protocole d\'API',
'settings.modeDaemon': 'CLI local',
'settings.modeDaemonHelp': 'Exécuter via un agent CLI sur votre machine',
'settings.modeDaemonOffline': 'Le daemon n\'est pas en cours d\'exécution',
'settings.modeDaemonOfflineMeta': 'daemon hors ligne',
'settings.modeDaemonInstalledMeta': '{count} installé(s)',
'settings.modeApi': 'API Anthropic',
'settings.modeApi': 'Fournisseur API',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Agent de code',
'settings.codeAgentHint':
'Détecté en analysant votre PATH. Choisissez le CLI à travers lequel les générations seront exécutées.',
'settings.rescan': '↻ Réanalyser',
'settings.rescanTitle': 'Réanalyser le PATH',
'settings.rescanRunning': 'Analyse...',
'settings.rescanSuccess': 'Analyse terminée. {count} disponible(s).',
'settings.rescanFailed': 'Lanalyse a échoué. Vérifiez le daemon et réessayez.',
'settings.noAgentsDetected':
'Aucun agent détecté pour l\'instant. Installez Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen ou GitHub Copilot CLI, puis cliquez sur Réanalyser.',
'settings.apiSection': 'API Anthropic',
'settings.quickFillProvider': 'Remplissage rapide du fournisseur',
'settings.customProvider': 'Fournisseur personnalisé',
'settings.apiKey': 'Clé API',
'settings.showKey': 'Afficher la clé',
'settings.hideKey': 'Masquer la clé',
'settings.show': 'Afficher',
'settings.hide': 'Masquer',
'settings.model': 'Modèle',
'settings.suggestedModelsHint':
'Ce sont des modèles suggérés pour ce protocole. Votre fournisseur peut prendre en charge d\'autres modèles.',
'settings.baseUrl': 'URL de base',
'settings.baseUrlInvalid': 'Saisissez une URL publique http:// ou https:// valide. Localhost est autorisé ; les IP de réseau privé sont bloquées.',
'settings.azureDeploymentModel': 'Nom du déploiement',
'settings.azureDeploymentModelHint':
'Pour Azure OpenAI, ce champ est utilisé comme nom du déploiement dans /openai/deployments/<model>. Saisissez le nom du déploiement créé dans Azure.',
'settings.apiVersion': 'Version API',
'settings.maxTokens': 'Tokens max (optionnel)',
'settings.maxTokensHint':
'Limite de la longueur de réponse. Chaque modèle a une valeur par défaut (affichée en placeholder) ; laissez vide pour l\'utiliser, ou entrez un nombre pour la remplacer.',
'settings.apiHint':
'Les appels vont directement de ce navigateur vers l\'URL de base que vous définissez. Pas de proxy. La clé ne quitte jamais localStorage.',
'settings.apiHint': 'Les appels passent par le proxy du daemon local vers la Base URL définie. La clé est stockée uniquement dans ce navigateur et envoyée avec les requêtes au fournisseur.',
'settings.skipForNow': 'Passer pour l\'instant',
'settings.getStarted': 'Commencer',
'settings.envConfigure': 'Configurer le mode d\'exécution',
@ -319,7 +330,7 @@ export const fr: Dict = {
'avatar.localCli': 'CLI local',
'avatar.anthropicApi': 'API Anthropic',
'avatar.useLocal': 'Utiliser le CLI local',
'avatar.useApi': 'Utiliser l\'API Anthropic',
'avatar.useApi': 'Utiliser API · BYOK',
'avatar.codeAgent': 'Agent de code',
'avatar.rescan': 'Réanalyser le PATH',
'avatar.settings': 'Paramètres',
@ -595,7 +606,7 @@ export const fr: Dict = {
'agentPicker.modeChoose': 'Choisir le mode d\'exécution',
'agentPicker.localCli': 'CLI local',
'agentPicker.daemonOff': 'daemon arrêté',
'agentPicker.byok': 'API Anthropic · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Sélectionner un agent CLI détecté',
'agentPicker.noAgents': 'aucun agent dans le PATH',
'agentPicker.notInstalled': 'non installé',

View file

@ -48,36 +48,47 @@ export const hu: Dict = {
'Válaszd ki, hogyan szeretnéd futtatni a generálásokat. Ezt bármikor módosíthatod a felső sáv Beállítások gombjával.',
'settings.kicker': 'Beállítások',
'settings.title': 'Végrehajtás és modell',
'settings.subtitle':
'Válassz a helyi code-agent CLI és az Anthropic API (BYOK) között. Az API-kulcs csak ebben a böngészőben tárolódik.',
'settings.subtitle': 'Válassz helyi CLI és BYOK között. Az API-kulcs csak ebben a böngészőben tárolódik.',
'settings.modeAria': 'Végrehajtási mód',
'settings.protocolAria': 'API protokoll',
'settings.modeDaemon': 'Helyi CLI',
'settings.modeDaemonHelp': 'Futtatás a gépeden lévő code-agent CLI-n keresztül',
'settings.modeDaemonOffline': 'A daemon nem fut',
'settings.modeDaemonOfflineMeta': 'a daemon offline',
'settings.modeDaemonInstalledMeta': '{count} telepítve',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API-szolgáltató',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Code agent',
'settings.codeAgentHint':
'A PATH átvizsgálásával észlelve. Válaszd ki a CLI-t, amelyen át a generálásokat szeretnéd futtatni.',
'settings.rescan': '↻ Újraellenőrzés',
'settings.rescanTitle': 'PATH újraellenőrzése',
'settings.rescanRunning': 'Ellenőrzés...',
'settings.rescanSuccess': 'Ellenőrzés kész. {count} elérhető.',
'settings.rescanFailed': 'Az ellenőrzés sikertelen. Ellenőrizd a daemont, majd próbáld újra.',
'settings.noAgentsDetected':
'Még nincs észlelt ügynök. Telepítsd a Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen vagy GitHub Copilot CLI valamelyikét, majd kattints az Újraellenőrzésre.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Szolgáltató gyors kitöltése',
'settings.customProvider': 'Egyéni szolgáltató',
'settings.apiKey': 'API-kulcs',
'settings.showKey': 'Kulcs megjelenítése',
'settings.hideKey': 'Kulcs elrejtése',
'settings.show': 'Megjelenítés',
'settings.hide': 'Elrejtés',
'settings.model': 'Modell',
'settings.suggestedModelsHint':
'Ezek a protokollhoz javasolt modellek. A szolgáltatód más modelleket is támogathat.',
'settings.baseUrl': 'Base URL',
'settings.baseUrlInvalid': 'Adj meg egy érvényes nyilvános http:// vagy https:// URL-t. A localhost engedélyezett; a privát hálózati IP-k blokkolva vannak.',
'settings.azureDeploymentModel': 'Deployment név',
'settings.azureDeploymentModelHint':
'Azure OpenAI esetén ez a mező a /openai/deployments/<model> deployment neveként szerepel. Add meg az Azure-ban létrehozott deployment nevét.',
'settings.apiVersion': 'API-verzió',
'settings.maxTokens': 'Max tokenek (opcionális)',
'settings.maxTokensHint':
'A válasz hosszának felső határa. Minden modellnek van hangolt alapértelmezése (placeholderként látható); hagyd üresen az alkalmazásához, vagy adj meg számot a felülíráshoz.',
'settings.apiHint':
'A hívások közvetlenül ebből a böngészőből mennek a megadott bázis URL-re. Nincs proxy. A kulcs sosem hagyja el a localStorage-t.',
'settings.apiHint': 'A hívások a helyi daemon proxyn keresztül mennek a beállított Base URL-re. A kulcs csak ebben a böngészőben tárolódik, és a szolgáltatói kérésekkel együtt kerül elküldésre.',
'settings.skipForNow': 'Most kihagyom',
'settings.getStarted': 'Kezdjük',
'settings.envConfigure': 'Végrehajtási mód beállítása',
@ -319,7 +330,7 @@ export const hu: Dict = {
'avatar.localCli': 'Helyi CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Helyi CLI használata',
'avatar.useApi': 'Anthropic API használata',
'avatar.useApi': 'API · BYOK használata',
'avatar.codeAgent': 'Kód-ügynök',
'avatar.rescan': 'PATH újraellenőrzése',
'avatar.settings': 'Beállítások',
@ -595,7 +606,7 @@ export const hu: Dict = {
'agentPicker.modeChoose': 'Válassz végrehajtási módot',
'agentPicker.localCli': 'Helyi CLI',
'agentPicker.daemonOff': 'a daemon kikapcsolva',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Válassz egy észlelt code-agent CLI-t',
'agentPicker.noAgents': 'nincs ügynök a PATH-on',
'agentPicker.notInstalled': 'nincs telepítve',

View file

@ -48,36 +48,47 @@ export const ja: Dict = {
'生成の実行方法を選んでください。この設定はいつでもトップバーの設定ボタンから変更できます。',
'settings.kicker': '設定',
'settings.title': '実行モデル',
'settings.subtitle':
'ローカルのコードエージェント CLI と Anthropic API (BYOK) から選択してください。APIキーはこのブラウザにのみ保存されます。',
'settings.subtitle': 'ローカル CLI と BYOK のどちらを使うか選択します。API キーはこのブラウザ内にのみ保存されます。',
'settings.modeAria': '実行モード',
'settings.protocolAria': 'API プロトコル',
'settings.modeDaemon': 'ローカル CLI',
'settings.modeDaemonHelp': 'マシン上のコードエージェント CLI 経由で実行',
'settings.modeDaemonOffline': 'デーモンが起動していません',
'settings.modeDaemonOfflineMeta': 'デーモンオフライン',
'settings.modeDaemonInstalledMeta': '{count} インストール済み',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API プロバイダー',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'コードエージェント',
'settings.codeAgentHint':
'PATH をスキャンして検出されます。生成に使用する CLI を選択してください。',
'settings.rescan': '↻ 再スキャン',
'settings.rescanTitle': 'PATH を再スキャン',
'settings.rescanRunning': 'スキャン中...',
'settings.rescanSuccess': 'スキャン完了。{count} 件が利用可能です。',
'settings.rescanFailed': 'スキャンに失敗しました。デーモンを確認して再試行してください。',
'settings.noAgentsDetected':
'エージェントが検出されませんでした。Claude Code、Codex、Gemini CLI、OpenCode、Cursor Agent、Qwen、または GitHub Copilot CLI のいずれかをインストールして、再スキャンをクリックしてください。',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'プロバイダーをクイック入力',
'settings.customProvider': 'カスタムプロバイダー',
'settings.apiKey': 'APIキー',
'settings.showKey': 'キーを表示',
'settings.hideKey': 'キーを隠す',
'settings.show': '表示',
'settings.hide': '隠す',
'settings.model': 'モデル',
'settings.suggestedModelsHint':
'これはこのプロトコル向けの推奨モデルです。プロバイダーによっては別のモデルをサポートしている場合があります。',
'settings.baseUrl': 'ベース URL',
'settings.baseUrlInvalid': '有効な公開 http:// または https:// URL を入力してください。localhost は許可され、プライベートネットワーク IP はブロックされます。',
'settings.azureDeploymentModel': 'デプロイ名',
'settings.azureDeploymentModelHint':
'Azure OpenAI では、このフィールドが /openai/deployments/<model> のデプロイ名として使われます。Azure で作成したデプロイ名を入力してください。',
'settings.apiVersion': 'API バージョン',
'settings.maxTokens': '最大トークン(任意)',
'settings.maxTokensHint':
'応答長の上限。各モデルにチューニング済みのデフォルト値があります(プレースホルダーに表示)。空のままにすればそれを使用し、数値を入力すれば上書きされます。',
'settings.apiHint':
'リクエストはこのブラウザから設定したベース URL に直接送信されます。プロキシなし。キーは localStorage から外に出ません。',
'settings.apiHint': 'リクエストはローカル daemon プロキシ経由で設定した Base URL に送信されます。キーはこのブラウザ内にのみ保存され、プロバイダーへのリクエスト時に送信されます。',
'settings.skipForNow': '今はスキップ',
'settings.getStarted': '始める',
'settings.envConfigure': '実行モードを設定',
@ -318,7 +329,7 @@ export const ja: Dict = {
'avatar.localCli': 'ローカル CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'ローカル CLI を使用',
'avatar.useApi': 'Anthropic API を使用',
'avatar.useApi': 'API · BYOK を使用',
'avatar.codeAgent': 'コードエージェント',
'avatar.rescan': 'PATH を再スキャン',
'avatar.settings': '設定',
@ -594,7 +605,7 @@ export const ja: Dict = {
'agentPicker.modeChoose': '実行モードを選択',
'agentPicker.localCli': 'ローカル CLI',
'agentPicker.daemonOff': 'デーモン停止中',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': '検出されたコードエージェント CLI を選択',
'agentPicker.noAgents': 'PATH 上にエージェントなし',
'agentPicker.notInstalled': '未インストール',

View file

@ -48,36 +48,47 @@ export const ko: Dict = {
"생성을 실행할 방법을 선택하세요. 상단 바의 Settings 버튼을 통해 언제든지 변경할 수 있습니다.",
'settings.kicker': '설정',
'settings.title': '실행 및 모델',
'settings.subtitle':
'로컬 코드 에이전트 CLI와 Anthropic API (BYOK) 중 하나를 선택하세요. API 키는 브라우저에만 저장됩니다.',
'settings.subtitle': '로컬 CLI와 BYOK 중에서 선택하세요. API 키는 이 브라우저에만 저장됩니다.',
'settings.modeAria': '실행 모드',
'settings.protocolAria': 'API 프로토콜',
'settings.modeDaemon': '로컬 CLI',
'settings.modeDaemonHelp': '사용자 기기의 코드 에이전트 CLI를 통해 실행합니다',
'settings.modeDaemonOffline': '데몬이 실행 중이 아닙니다',
'settings.modeDaemonOfflineMeta': '데몬 오프라인',
'settings.modeDaemonInstalledMeta': '{count}개 설치됨',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API 제공자',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': '코드 에이전트',
'settings.codeAgentHint':
'PATH 환경 변수 스캔을 통해 감지됩니다. 생성을 처리할 CLI를 선택하세요.',
'settings.rescan': '↻ 다시 스캔',
'settings.rescanTitle': 'PATH 다시 스캔',
'settings.rescanRunning': '스캔 중...',
'settings.rescanSuccess': '스캔 완료. {count}개 사용 가능.',
'settings.rescanFailed': '스캔에 실패했습니다. 데몬을 확인한 후 다시 시도하세요.',
'settings.noAgentsDetected':
'에이전트가 감지되지 않았습니다. Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen 또는 GitHub Copilot CLI 중 하나를 설치한 후 다시 스캔을 클릭하세요.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': '제공자 빠른 입력',
'settings.customProvider': '사용자 지정 제공자',
'settings.apiKey': 'API 키',
'settings.showKey': '키 표시',
'settings.hideKey': '키 숨기기',
'settings.show': '표시',
'settings.hide': '숨기기',
'settings.model': '모델',
'settings.suggestedModelsHint':
'이 프로토콜에 대한 추천 모델입니다. 사용 중인 제공자는 다른 모델을 지원할 수 있습니다.',
'settings.maxTokens': '최대 토큰 수 (선택 사항)',
'settings.maxTokensHint':
'응답 길이 상한입니다. 각 모델에는 기본값이 미리 조정되어 있으며(placeholder로 표시됨), 비워 두면 그 값을 사용하고 숫자를 입력하면 덮어씁니다.',
'settings.baseUrl': 'Base URL',
'settings.apiHint':
'이 브라우저에서 설정한 Base URL로 직접 호출됩니다. 프록시는 사용되지 않으며, 키는 localStorage에만 보관됩니다.',
'settings.baseUrlInvalid': '유효한 공개 http:// 또는 https:// URL을 입력하세요. localhost는 허용되며 사설 네트워크 IP는 차단됩니다.',
'settings.azureDeploymentModel': '배포 이름',
'settings.azureDeploymentModelHint':
'Azure OpenAI에서는 이 필드가 /openai/deployments/<model>의 배포 이름으로 사용됩니다. Azure에서 만든 배포 이름을 입력하세요.',
'settings.apiVersion': 'API 버전',
'settings.apiHint': '요청은 로컬 daemon 프록시를 통해 설정한 Base URL로 전송됩니다. 키는 이 브라우저에만 저장되며 제공자 요청과 함께 전송됩니다.',
'settings.skipForNow': '지금은 건너뛰기',
'settings.getStarted': '시작하기',
'settings.envConfigure': '실행 모드 구성',
@ -319,7 +330,7 @@ export const ko: Dict = {
'avatar.localCli': '로컬 CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': '로컬 CLI 사용',
'avatar.useApi': 'Anthropic API 사용',
'avatar.useApi': 'API · BYOK 사용',
'avatar.codeAgent': '코드 에이전트',
'avatar.rescan': 'PATH 다시 스캔',
'avatar.settings': '설정',
@ -595,7 +606,7 @@ export const ko: Dict = {
'agentPicker.modeChoose': '실행 모드 선택',
'agentPicker.localCli': '로컬 CLI',
'agentPicker.daemonOff': '데몬 꺼짐',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': '감지된 코드 에이전트 CLI 선택',
'agentPicker.noAgents': 'PATH에서 에이전트를 찾을 수 없음',
'agentPicker.notInstalled': '설치되지 않음',

View file

@ -48,36 +48,47 @@ export const pl: Dict = {
"Wybierz sposób generowania projektów. Możesz to zmienić w dowolnym momencie w Ustawieniach na górnym pasku.",
'settings.kicker': 'Ustawienia',
'settings.title': 'Wykonanie i model',
'settings.subtitle':
'Wybierz między lokalnym agentem CLI a Anthropic API (własny klucz). Twój klucz API jest przechowywany tylko w tej przeglądarce.',
'settings.subtitle': 'Wybierz lokalne CLI albo BYOK. Klucz API jest przechowywany tylko w tej przeglądarce.',
'settings.modeAria': 'Tryb wykonywania',
'settings.protocolAria': 'Protokół API',
'settings.modeDaemon': 'Lokalne CLI',
'settings.modeDaemonHelp': 'Uruchamiaj przez agenta CLI na swoim komputerze',
'settings.modeDaemonOffline': 'Daemon nie jest uruchomiony',
'settings.modeDaemonOfflineMeta': 'daemon offline',
'settings.modeDaemonInstalledMeta': '{count} zainstalowano',
'settings.modeApi': 'Anthropic API',
'settings.modeApiMeta': 'Własny klucz',
'settings.modeApi': 'Dostawca API',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Agent kodu',
'settings.codeAgentHint':
'Wykryto poprzez skanowanie PATH. Wybierz CLI, przez które mają przechodzić generacje.',
'settings.rescan': '↻ Ponów skanowanie',
'settings.rescanTitle': 'Ponownie skanuj PATH',
'settings.rescanRunning': 'Skanowanie...',
'settings.rescanSuccess': 'Skanowanie zakończone. Dostępne: {count}.',
'settings.rescanFailed': 'Skanowanie nie powiodło się. Sprawdź daemon i spróbuj ponownie.',
'settings.noAgentsDetected':
'Nie wykryto jeszcze żadnych agentów. Zainstaluj Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen lub GitHub Copilot CLI, a następnie kliknij Ponów skanowanie.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Szybkie wypełnienie dostawcy',
'settings.customProvider': 'Niestandardowy dostawca',
'settings.apiKey': 'Klucz API',
'settings.showKey': 'Pokaż klucz',
'settings.hideKey': 'Ukryj klucz',
'settings.show': 'Pokaż',
'settings.hide': 'Ukryj',
'settings.model': 'Model',
'settings.suggestedModelsHint':
'To sugerowane modele dla tego protokołu. Twój dostawca może obsługiwać inne modele.',
'settings.baseUrl': 'Bazowy URL',
'settings.baseUrlInvalid': 'Wpisz poprawny publiczny URL http:// lub https://. Localhost jest dozwolony; prywatne adresy IP są blokowane.',
'settings.azureDeploymentModel': 'Nazwa wdrożenia',
'settings.azureDeploymentModelHint':
'Dla Azure OpenAI to pole jest używane jako nazwa wdrożenia w /openai/deployments/<model>. Wpisz nazwę wdrożenia utworzonego w Azure.',
'settings.apiVersion': 'Wersja API',
'settings.maxTokens': 'Maks. liczba tokenów (opcjonalnie)',
'settings.maxTokensHint':
'Limit długości odpowiedzi. Każdy model ma dostrojony domyślny limit (widoczny jako placeholder); pozostaw puste, aby go użyć, lub wpisz liczbę.',
'settings.apiHint':
'Zapytania idą bezpośrednio z przeglądarki do ustawionego adresu URL. Bez proxy. Klucz nigdy nie opuszcza localStorage.',
'settings.apiHint': 'Wywołania są wysyłane przez lokalny proxy daemon do ustawionego Base URL. Klucz jest przechowywany tylko w tej przeglądarce i wysyłany z żądaniami do dostawcy.',
'settings.skipForNow': 'Pomiń na razie',
'settings.getStarted': 'Rozpocznij',
'settings.envConfigure': 'Skonfiguruj tryb wykonywania',
@ -319,7 +330,7 @@ export const pl: Dict = {
'avatar.localCli': 'Lokalne CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Użyj lokalnego CLI',
'avatar.useApi': 'Użyj Anthropic API',
'avatar.useApi': 'Użyj API · BYOK',
'avatar.codeAgent': 'Agent kodu',
'avatar.rescan': 'Skanuj PATH ponownie',
'avatar.settings': 'Ustawienia',
@ -595,7 +606,7 @@ export const pl: Dict = {
'agentPicker.modeChoose': 'Wybierz tryb wykonywania',
'agentPicker.localCli': 'Lokalne CLI',
'agentPicker.daemonOff': 'daemon wyłączony',
'agentPicker.byok': 'Anthropic API · Własny klucz',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Wybierz wykrytego agenta CLI',
'agentPicker.noAgents': 'brak agentów w PATH',
'agentPicker.notInstalled': 'nie zainstalowano',

View file

@ -48,36 +48,47 @@ export const ptBR: Dict = {
'Escolha como você quer executar as gerações. Você pode mudar isso a qualquer momento no botão Configurações da barra superior.',
'settings.kicker': 'Configurações',
'settings.title': 'Execução e modelo',
'settings.subtitle':
'Escolha entre uma CLI local de agente de código e a API da Anthropic (BYOK). Sua chave de API fica salva apenas neste navegador.',
'settings.subtitle': 'Escolha entre CLI local e BYOK. Sua chave de API fica armazenada apenas neste navegador.',
'settings.modeAria': 'Modo de execução',
'settings.protocolAria': 'Protocolo de API',
'settings.modeDaemon': 'CLI local',
'settings.modeDaemonHelp': 'Execute por uma CLI de agente de código na sua máquina',
'settings.modeDaemonOffline': 'Daemon não está em execução',
'settings.modeDaemonOfflineMeta': 'daemon offline',
'settings.modeDaemonInstalledMeta': '{count} instalados',
'settings.modeApi': 'API da Anthropic',
'settings.modeApi': 'Provedor de API',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Agente de código',
'settings.codeAgentHint':
'Detectado ao escanear seu PATH. Escolha a CLI por onde as gerações devem passar.',
'settings.rescan': '↻ Reescanear',
'settings.rescanTitle': 'Reescanear PATH',
'settings.rescanRunning': 'Escaneando...',
'settings.rescanSuccess': 'Escaneamento concluido. {count} disponiveis.',
'settings.rescanFailed': 'Falha ao escanear. Verifique o daemon e tente novamente.',
'settings.noAgentsDetected':
'Nenhum agente detectado ainda. Instale Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen ou GitHub Copilot CLI e clique em Reescanear.',
'settings.apiSection': 'API da Anthropic',
'settings.quickFillProvider': 'Preencher provedor',
'settings.customProvider': 'Provedor personalizado',
'settings.apiKey': 'Chave de API',
'settings.showKey': 'Mostrar chave',
'settings.hideKey': 'Ocultar chave',
'settings.show': 'Mostrar',
'settings.hide': 'Ocultar',
'settings.model': 'Modelo',
'settings.suggestedModelsHint':
'Estes são modelos sugeridos para este protocolo. Seu provedor pode oferecer suporte a modelos diferentes.',
'settings.baseUrl': 'URL base',
'settings.baseUrlInvalid': 'Informe uma URL pública http:// ou https:// válida. Localhost é permitido; IPs de rede privada são bloqueados.',
'settings.azureDeploymentModel': 'Nome do deployment',
'settings.azureDeploymentModelHint':
'No Azure OpenAI, este campo e usado como nome do deployment em /openai/deployments/<model>. Informe o nome do deployment criado no Azure.',
'settings.apiVersion': 'Versão da API',
'settings.maxTokens': 'Tokens máx. (opcional)',
'settings.maxTokensHint':
'Limite para o comprimento da resposta. Cada modelo tem um valor padrão ajustado (visível no placeholder); deixe em branco para usá-lo ou insira um número para substituí-lo.',
'settings.apiHint':
'As chamadas vão direto deste navegador para a URL base definida. Sem proxy. A chave nunca sai do localStorage.',
'settings.apiHint': 'As chamadas passam pelo proxy do daemon local até a Base URL definida. A chave fica armazenada apenas neste navegador e é enviada com as requisições ao provedor.',
'settings.skipForNow': 'Pular por enquanto',
'settings.getStarted': 'Começar',
'settings.envConfigure': 'Configurar modo de execução',
@ -318,7 +329,7 @@ export const ptBR: Dict = {
'avatar.localCli': 'CLI local',
'avatar.anthropicApi': 'API da Anthropic',
'avatar.useLocal': 'Usar CLI local',
'avatar.useApi': 'Usar API da Anthropic',
'avatar.useApi': 'Usar API · BYOK',
'avatar.codeAgent': 'Agente de código',
'avatar.rescan': 'Reescanear PATH',
'avatar.settings': 'Configurações',
@ -594,7 +605,7 @@ export const ptBR: Dict = {
'agentPicker.modeChoose': 'Escolha o modo de execução',
'agentPicker.localCli': 'CLI local',
'agentPicker.daemonOff': 'daemon desligado',
'agentPicker.byok': 'API da Anthropic · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Selecione uma CLI de agente de código detectada',
'agentPicker.noAgents': 'nenhum agente no PATH',
'agentPicker.notInstalled': 'não instalado',

View file

@ -48,36 +48,47 @@ export const ru: Dict = {
'Выберите, как запускать генерации. Вы можете изменить это в любое время через кнопку Настройки в верхней панели.',
'settings.kicker': 'Настройки',
'settings.title': 'Выполнение и модель',
'settings.subtitle':
'Выберите локальный CLI код-агента или Anthropic API (BYOK). API-ключ хранится только в этом браузере.',
'settings.subtitle': 'Выберите локальный CLI или BYOK. Ваш API-ключ хранится только в этом браузере.',
'settings.modeAria': 'Режим выполнения',
'settings.protocolAria': 'Протокол API',
'settings.modeDaemon': 'Локальный CLI',
'settings.modeDaemonHelp': 'Запуск через CLI код-агента на вашем компьютере',
'settings.modeDaemonOffline': 'Демон не запущен',
'settings.modeDaemonOfflineMeta': 'демон офлайн',
'settings.modeDaemonInstalledMeta': '{count} установлено',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API-провайдер',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Код-агент',
'settings.codeAgentHint':
'Определяется сканированием вашего PATH. Выберите CLI, через который будут проходить генерации.',
'settings.rescan': '↻ Пересканировать',
'settings.rescanTitle': 'Пересканировать PATH',
'settings.rescanRunning': 'Сканирование...',
'settings.rescanSuccess': 'Сканирование завершено. Доступно: {count}.',
'settings.rescanFailed': 'Сканирование не удалось. Проверьте демон и повторите попытку.',
'settings.noAgentsDetected':
'Агенты ещё не обнаружены. Установите один из следующих инструментов: Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen или GitHub Copilot CLI, затем нажмите «Пересканировать».',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Быстро заполнить провайдера',
'settings.customProvider': 'Пользовательский провайдер',
'settings.apiKey': 'API-ключ',
'settings.showKey': 'Показать ключ',
'settings.hideKey': 'Скрыть ключ',
'settings.show': 'Показать',
'settings.hide': 'Скрыть',
'settings.model': 'Модель',
'settings.suggestedModelsHint':
'Это рекомендуемые модели для этого протокола. Ваш провайдер может поддерживать другие модели.',
'settings.baseUrl': 'Базовый URL',
'settings.baseUrlInvalid': 'Введите корректный публичный URL с http:// или https://. Localhost разрешен; IP частных сетей блокируются.',
'settings.azureDeploymentModel': 'Имя развертывания',
'settings.azureDeploymentModelHint':
'Для Azure OpenAI это поле используется как имя развертывания в /openai/deployments/<model>. Укажите имя развертывания, созданного в Azure.',
'settings.apiVersion': 'Версия API',
'settings.maxTokens': 'Макс. токенов (опционально)',
'settings.maxTokensHint':
'Ограничение длины ответа. У каждой модели свой настроенный дефолт (виден в плейсхолдере); оставьте поле пустым, чтобы использовать его, или введите число, чтобы переопределить.',
'settings.apiHint':
'Вызовы идут напрямую из этого браузера на указанный вами базовый URL. Без прокси. Ключ никогда не покидает localStorage.',
'settings.apiHint': 'Запросы отправляются через локальный прокси daemon на указанную Base URL. Ключ хранится только в этом браузере и отправляется в запросах к провайдеру.',
'settings.skipForNow': 'Пропустить сейчас',
'settings.getStarted': 'Начать',
'settings.envConfigure': 'Настроить режим выполнения',
@ -318,7 +329,7 @@ export const ru: Dict = {
'avatar.localCli': 'Локальный CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Использовать локальный CLI',
'avatar.useApi': 'Использовать Anthropic API',
'avatar.useApi': 'Использовать API · BYOK',
'avatar.codeAgent': 'Код-агент',
'avatar.rescan': 'Пересканировать PATH',
'avatar.settings': 'Настройки',
@ -594,7 +605,7 @@ export const ru: Dict = {
'agentPicker.modeChoose': 'Выберите режим выполнения',
'agentPicker.localCli': 'Локальный CLI',
'agentPicker.daemonOff': 'демон выключен',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Выберите обнаруженный CLI код-агента',
'agentPicker.noAgents': 'нет агентов в PATH',
'agentPicker.notInstalled': 'не установлено',

View file

@ -48,36 +48,47 @@ export const tr: Dict = {
"Oluşturmaları nasıl çalıştıracağınızı seçin. Bunu her zaman üst çubuktaki Ayarlar sekmesinden değiştirebilirsiniz.",
'settings.kicker': 'Ayarlar',
'settings.title': 'Çalıştırma & model',
'settings.subtitle':
'Yerel bir CLI kod ajanı veya Anthropic API (BYOK) arasında seçim yapın. API anahtarınız yalnızca bu tarayıcıda saklanır.',
'settings.subtitle': 'Yerel CLI ile BYOK arasında seçim yapın. API anahtarınız yalnızca bu tarayıcıda saklanır.',
'settings.modeAria': 'Çalıştırma modu',
'settings.protocolAria': 'API protokolü',
'settings.modeDaemon': 'Yerel CLI',
'settings.modeDaemonHelp': 'Yerel kod ajanı CLI ile çalıştır',
'settings.modeDaemonOffline': 'Arka plan servisi çalışmıyor',
'settings.modeDaemonOfflineMeta': 'Arka plan servisi devre dışı',
'settings.modeDaemonInstalledMeta': '{count} kuruldu',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API sağlayıcısı',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Kod ajanı',
'settings.codeAgentHint':
'Yerel PATH taranarak tespit edildi. Oluşum akışının yaşanacağı CLI aracınızı seçin.',
'settings.rescan': '↻ Yeniden tara',
'settings.rescanTitle': 'PATHı yeniden tara',
'settings.rescanRunning': 'Taranıyor...',
'settings.rescanSuccess': 'Tarama tamamlandı. {count} kullanılabilir.',
'settings.rescanFailed': 'Tarama başarısız. Daemonu kontrol edip tekrar deneyin.',
'settings.noAgentsDetected':
'Hiçbir ajan tespit edilemedi. Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen, veya GitHub Copilot CLIlardan birini kurun ve yeniden tarayın.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Sağlayıcıyı hızlı doldur',
'settings.customProvider': 'Özel sağlayıcı',
'settings.apiKey': 'API anahtarı',
'settings.showKey': 'Anahtarı göster',
'settings.hideKey': 'Anahtarı gizle',
'settings.show': 'Göster',
'settings.hide': 'Gizle',
'settings.model': 'Model',
'settings.suggestedModelsHint':
'Bunlar bu protokol için önerilen modellerdir. Sağlayıcınız farklı modelleri destekleyebilir.',
'settings.baseUrl': 'Temel URL',
'settings.baseUrlInvalid': 'Geçerli bir genel http:// veya https:// URL girin. Localhost izinlidir; özel ağ IPleri engellenir.',
'settings.azureDeploymentModel': 'Dağıtım adı',
'settings.azureDeploymentModelHint':
'Azure OpenAI icin bu alan /openai/deployments/<model> icindeki dagitim adi olarak kullanilir. Azureda olusturdugunuz dagitim adini girin.',
'settings.apiVersion': 'API sürümü',
'settings.maxTokens': 'Maks. token (isteğe bağlı)',
'settings.maxTokensHint':
'Yanıt uzunluğu sınırı. Her modelin ayarlanmış bir varsayılanı vardır (yer tutucuda görünür); kullanmak için boş bırakın, üzerine yazmak için bir sayı girin.',
'settings.apiHint':
'Çağrılar, bu tarayıcıdan doğrudan belirlediğiniz temel URLye yönlendirilir. Vekil sunucu kullanılmaz. Anahtar hiçbir zaman localStoragedan ayrılmaz.',
'settings.apiHint': 'İstekler yerel daemon proxy üzerinden ayarladığınız Base URLye gönderilir. Anahtar yalnızca bu tarayıcıda saklanır ve sağlayıcı istekleriyle birlikte gönderilir.',
'settings.skipForNow': 'Şimdilik atla',
'settings.getStarted': 'Başla',
'settings.envConfigure': 'Yürütme modunu ayarlayın',
@ -318,7 +329,7 @@ export const tr: Dict = {
'avatar.localCli': 'Yerel CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Yerel CLIı kullan',
'avatar.useApi': 'Anthropic APIı kullan',
'avatar.useApi': 'API · BYOK kullan',
'avatar.codeAgent': 'Kod ajanı',
'avatar.rescan': 'PATHı yeniden tara',
'avatar.settings': 'Ayarlar',
@ -594,7 +605,7 @@ export const tr: Dict = {
'agentPicker.modeChoose': 'Yürütme modunu seç',
'agentPicker.localCli': 'Yerel CLI',
'agentPicker.daemonOff': 'Arka plan servisi kapalı',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Tespit edilmiş bir kod ajanı CLIı seçin',
'agentPicker.noAgents': 'PATHte ajan yok',
'agentPicker.notInstalled': 'kurulmadı',

View file

@ -48,36 +48,48 @@ export const uk: Dict = {
'Виберіть, як ви хочете запускати генерацію. Ви можете змінити це в будь-який час за допомогою кнопки Налаштування в верхній панелі.',
'settings.kicker': 'Налаштування',
'settings.title': 'Виконання та модель',
'settings.subtitle':
'Виберіть між локальним CLI кодового агента та Anthropic API (BYOK). Ваш API-ключ зберігається лише в цьому браузері.',
'settings.subtitle': 'Виберіть локальний CLI або BYOK. Ваш API-ключ зберігається лише в цьому браузері.',
'settings.modeAria': 'Режим виконання',
'settings.protocolAria': 'Протокол API',
'settings.modeDaemon': 'Локальний CLI',
'settings.modeDaemonHelp': 'Запуск через CLI кодового агента на вашому комп\'ютері',
'settings.modeDaemonOffline': 'Фоновий процес не запущений',
'settings.modeDaemonOfflineMeta': 'Фоновий процес офлайн',
'settings.modeDaemonInstalledMeta': '{count} встановлено',
'settings.modeApi': 'Anthropic API',
'settings.modeApi': 'API-провайдер',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Кодовий агент',
'settings.codeAgentHint':
'Виявляється за допомогою сканування вашого PATH. Виберіть CLI, через який мають проходити генерації.',
'settings.rescan': '↻ Переканувати',
'settings.rescanTitle': 'Повторно сканувати PATH',
'settings.rescanRunning': 'Сканування...',
'settings.rescanSuccess': 'Сканування завершено. Доступно: {count}.',
'settings.rescanFailed':
'Сканування не вдалося. Перевірте фоновий процес і спробуйте ще раз.',
'settings.noAgentsDetected':
'Агентів ще не виявлено. Встановіть один з: Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen або GitHub Copilot CLI, а потім натисніть Переканувати.',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': 'Швидко заповнити провайдера',
'settings.customProvider': 'Власний провайдер',
'settings.apiKey': 'API-ключ',
'settings.showKey': 'Показати ключ',
'settings.hideKey': 'Приховати ключ',
'settings.show': 'Показати',
'settings.hide': 'Приховати',
'settings.model': 'Модель',
'settings.suggestedModelsHint':
'Це рекомендовані моделі для цього протоколу. Ваш провайдер може підтримувати інші моделі.',
'settings.baseUrl': 'Базовий URL',
'settings.baseUrlInvalid': 'Введіть дійсний публічний URL з http:// або https://. Localhost дозволено; IP приватних мереж блокуються.',
'settings.azureDeploymentModel': 'Назва розгортання',
'settings.azureDeploymentModelHint':
'Для Azure OpenAI це поле використовується як назва розгортання в /openai/deployments/<model>. Введіть назву розгортання, створену в Azure.',
'settings.apiVersion': 'Версія API',
'settings.maxTokens': 'Макс. токенів (необов\'язково)',
'settings.maxTokensHint':
'Обмеження на довжину відповіді. Кожна модель має налаштовану за замовчуванням (показано в заповнювачі); залиште поле порожнім, щоб використовувати її, або введіть число, щоб переопрацювати.',
'settings.apiHint':
'Виклики йдуть безпосередньо з цього браузера на установлений вами базовий URL. Без проксі. Ключ ніколи не залишає localStorage.',
'settings.apiHint': 'Запити надсилаються через локальний проксі daemon до вказаного Base URL. Ключ зберігається лише в цьому браузері й надсилається із запитами до провайдера.',
'settings.skipForNow': 'Пропустити зараз',
'settings.getStarted': 'Почати',
'settings.envConfigure': 'Налаштування режиму виконання',
@ -319,7 +331,7 @@ export const uk: Dict = {
'avatar.localCli': 'Локальний CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': 'Використовувати локальний CLI',
'avatar.useApi': 'Використовувати Anthropic API',
'avatar.useApi': 'Використовувати API · BYOK',
'avatar.codeAgent': 'Кодовий агент',
'avatar.rescan': 'Переканувати PATH',
'avatar.settings': 'Налаштування',
@ -595,7 +607,7 @@ export const uk: Dict = {
'agentPicker.modeChoose': 'Виберіть режим виконання',
'agentPicker.localCli': 'Локальний CLI',
'agentPicker.daemonOff': 'фоновий процес вимкнений',
'agentPicker.byok': 'Anthropic API · BYOK',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': 'Виберіть виявлений CLI кодового агента',
'agentPicker.noAgents': 'немає агентів на PATH',
'agentPicker.notInstalled': 'не встановлено',

View file

@ -48,35 +48,46 @@ export const zhCN: Dict = {
'选择你希望使用的执行方式。后续可以随时从顶部「设置」按钮中修改。',
'settings.kicker': '设置',
'settings.title': '执行模式与模型',
'settings.subtitle':
'在本机的代码代理 CLI 与 Anthropic API自带 Key之间切换。API Key 只保存在当前浏览器中。',
'settings.subtitle': '在本机 CLI 与 BYOK 之间选择。API Key 只保存在当前浏览器中。',
'settings.modeAria': '执行模式',
'settings.protocolAria': 'API 协议',
'settings.modeDaemon': '本机 CLI',
'settings.modeDaemonHelp': '通过本机的代码代理 CLI 执行',
'settings.modeDaemonOffline': '后台守护进程未运行',
'settings.modeDaemonOfflineMeta': '守护进程未运行',
'settings.modeDaemonInstalledMeta': '已安装 {count} 个',
'settings.modeApi': 'Anthropic API',
'settings.modeApiMeta': '自带 Key',
'settings.modeApi': 'API 提供方',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': '代码代理',
'settings.codeAgentHint': '通过扫描 PATH 自动检测,选择你希望使用的 CLI。',
'settings.rescan': '↻ 重新扫描',
'settings.rescanTitle': '重新扫描 PATH',
'settings.rescanRunning': '扫描中...',
'settings.rescanSuccess': '扫描完成,{count} 个可用。',
'settings.rescanFailed': '扫描失败,请检查后台守护进程后重试。',
'settings.noAgentsDetected':
'尚未检测到任何代理。请安装 Claude Code、Codex、Gemini CLI、OpenCode、Cursor Agent、Qwen 或 GitHub Copilot CLI 中的一个,然后点击「重新扫描」。',
'settings.apiSection': 'Anthropic API',
'settings.quickFillProvider': '快速填充提供方',
'settings.customProvider': '自定义提供方',
'settings.apiKey': 'API Key',
'settings.showKey': '显示 Key',
'settings.hideKey': '隐藏 Key',
'settings.show': '显示',
'settings.hide': '隐藏',
'settings.model': '模型',
'settings.suggestedModelsHint':
'这些是此协议的建议模型。你的提供方可能支持不同的模型。',
'settings.baseUrl': 'Base URL',
'settings.baseUrlInvalid': '请输入有效的公网 http:// 或 https:// URL。允许 localhost会阻止私有网络 IP。',
'settings.azureDeploymentModel': '部署名称',
'settings.azureDeploymentModelHint':
'对于 Azure OpenAI此字段会作为 /openai/deployments/<model> 中的部署名称使用。请填写你在 Azure 中创建的部署名称。',
'settings.apiVersion': 'API 版本',
'settings.maxTokens': '最大 tokens可选',
'settings.maxTokensHint':
'响应长度上限。每个 model 有调优过的默认值(在 placeholder 里显示),留空即使用,输入数字则覆盖。',
'settings.apiHint':
'请求会从当前浏览器直连你设置的 Base URL无中转代理。Key 只存放在 localStorage。',
'settings.apiHint': '请求会通过本机 daemon 代理发送到你设置的 Base URL。Key 只保存在当前浏览器中,并随提供方请求发送。',
'settings.skipForNow': '暂时跳过',
'settings.getStarted': '开始使用',
'settings.envConfigure': '配置执行模式',
@ -314,7 +325,7 @@ export const zhCN: Dict = {
'avatar.localCli': '本机 CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': '使用本机 CLI',
'avatar.useApi': '使用 Anthropic API',
'avatar.useApi': '使用 API · BYOK',
'avatar.codeAgent': '代码代理',
'avatar.rescan': '重新扫描 PATH',
'avatar.settings': '设置',
@ -579,7 +590,7 @@ export const zhCN: Dict = {
'agentPicker.modeChoose': '选择执行模式',
'agentPicker.localCli': '本机 CLI',
'agentPicker.daemonOff': '守护进程未运行',
'agentPicker.byok': 'Anthropic API · 自带 Key',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': '选择已检测到的代码代理 CLI',
'agentPicker.noAgents': 'PATH 中未发现代理',
'agentPicker.notInstalled': '未安装',

View file

@ -48,35 +48,46 @@ export const zhTW: Dict = {
'選擇你希望使用的執行方式。後續可以隨時從頂端「設定」按鈕中修改。',
'settings.kicker': '設定',
'settings.title': '執行模式與模型',
'settings.subtitle':
'在本機的程式碼代理 CLI 與 Anthropic API自帶 Key之間切換。API Key 只儲存在當前瀏覽器中。',
'settings.subtitle': '在本機 CLI 與 BYOK 之間選擇。API Key 只儲存在目前瀏覽器中。',
'settings.modeAria': '執行模式',
'settings.protocolAria': 'API 協定',
'settings.modeDaemon': '本機 CLI',
'settings.modeDaemonHelp': '透過本機的程式碼代理 CLI 執行',
'settings.modeDaemonOffline': '背景守護程序未執行',
'settings.modeDaemonOfflineMeta': '守護程序未執行',
'settings.modeDaemonInstalledMeta': '已安裝 {count} 個',
'settings.modeApi': 'API',
'settings.modeApiMeta': '自帶 Key',
'settings.modeApi': 'API 提供方',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': '程式碼代理',
'settings.codeAgentHint': '透過掃描 PATH 自動偵測,選擇你希望使用的 CLI。',
'settings.rescan': '↻ 重新掃描',
'settings.rescanTitle': '重新掃描 PATH',
'settings.rescanRunning': '掃描中...',
'settings.rescanSuccess': '掃描完成,{count} 個可用。',
'settings.rescanFailed': '掃描失敗,請檢查背景守護程序後重試。',
'settings.noAgentsDetected':
'尚未偵測到任何代理。請安裝 Claude Code、Codex、Gemini CLI、OpenCode、Cursor Agent 或 Qwen 其中之一,然後點擊「重新掃描」。',
'settings.apiSection': 'API 設定',
'settings.quickFillProvider': '快速填入提供方',
'settings.customProvider': '自訂提供方',
'settings.apiKey': 'API Key',
'settings.showKey': '顯示 Key',
'settings.hideKey': '隱藏 Key',
'settings.show': '顯示',
'settings.hide': '隱藏',
'settings.model': '模型',
'settings.suggestedModelsHint':
'這些是此協定的建議模型。你的提供方可能支援不同的模型。',
'settings.baseUrl': 'Base URL',
'settings.baseUrlInvalid': '請輸入有效的公網 http:// 或 https:// URL。允許 localhost會阻止私有網路 IP。',
'settings.azureDeploymentModel': '部署名稱',
'settings.azureDeploymentModelHint':
'對於 Azure OpenAI此欄位會作為 /openai/deployments/<model> 中的部署名稱使用。請填入你在 Azure 中建立的部署名稱。',
'settings.apiVersion': 'API 版本',
'settings.maxTokens': '最大 tokens可選',
'settings.maxTokensHint':
'回應長度上限。每個 model 有調過的預設值(在 placeholder 顯示),留空即使用,輸入數字則覆蓋。',
'settings.apiHint':
'請求會從當前瀏覽器直連你設定的 Base URL無中轉代理。Key 只存放在 localStorage。',
'settings.apiHint': '請求會透過本機 daemon 代理送到你設定的 Base URL。Key 只儲存在目前瀏覽器中,並隨提供方請求送出。',
'settings.skipForNow': '暫時跳過',
'settings.getStarted': '開始使用',
'settings.envConfigure': '設定執行模式',
@ -314,7 +325,7 @@ export const zhTW: Dict = {
'avatar.localCli': '本機 CLI',
'avatar.anthropicApi': 'Anthropic API',
'avatar.useLocal': '使用本機 CLI',
'avatar.useApi': '使用 Anthropic API',
'avatar.useApi': '使用 API · BYOK',
'avatar.codeAgent': '程式碼代理',
'avatar.rescan': '重新掃描 PATH',
'avatar.settings': '設定',
@ -579,7 +590,7 @@ export const zhTW: Dict = {
'agentPicker.modeChoose': '選擇執行模式',
'agentPicker.localCli': '本機 CLI',
'agentPicker.daemonOff': '守護程序未執行',
'agentPicker.byok': 'API · 自帶 Key',
'agentPicker.byok': 'API · BYOK',
'agentPicker.selectAgent': '選擇已偵測到的程式碼代理 CLI',
'agentPicker.noAgents': 'PATH 中未發現代理',
'agentPicker.notInstalled': '未安裝',

View file

@ -77,6 +77,7 @@ export interface Dict {
'settings.title': string;
'settings.subtitle': string;
'settings.modeAria': string;
'settings.protocolAria': string;
'settings.modeDaemon': string;
'settings.modeDaemonHelp': string;
'settings.modeDaemonOffline': string;
@ -88,17 +89,27 @@ export interface Dict {
'settings.codeAgentHint': string;
'settings.rescan': string;
'settings.rescanTitle': string;
'settings.rescanRunning': string;
'settings.rescanSuccess': string;
'settings.rescanFailed': string;
'settings.noAgentsDetected': string;
'settings.apiSection': string;
'settings.quickFillProvider': string;
'settings.customProvider': string;
'settings.apiKey': string;
'settings.showKey': string;
'settings.hideKey': string;
'settings.show': string;
'settings.hide': string;
'settings.model': string;
'settings.suggestedModelsHint': string;
'settings.maxTokens': string;
'settings.maxTokensHint': string;
'settings.baseUrl': string;
'settings.baseUrlInvalid': string;
'settings.azureDeploymentModel': string;
'settings.azureDeploymentModelHint': string;
'settings.apiVersion': string;
'settings.apiHint': string;
'settings.skipForNow': string;
'settings.getStarted': string;

View file

@ -1153,12 +1153,13 @@ code {
/* Segmented control */
.seg-control {
display: grid;
grid-template-columns: repeat(var(--seg-cols, 2), 1fr);
grid-template-columns: repeat(var(--seg-cols, 2), minmax(0, 1fr));
gap: 6px;
padding: 4px;
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
min-width: 0;
}
.seg-btn {
display: flex;
@ -1171,6 +1172,7 @@ code {
background: transparent;
cursor: pointer;
text-align: left;
min-width: 0;
}
.seg-btn:hover:not(:disabled):not(.active) { background: rgba(255, 255, 255, 0.5); }
.seg-btn.active {
@ -1178,11 +1180,82 @@ code {
border-color: var(--border-strong);
box-shadow: var(--shadow-sm);
}
.seg-btn .seg-title { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.seg-btn .seg-meta { font-size: 11px; color: var(--text-muted); letter-spacing: 0.01em; }
.seg-btn .seg-title { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.seg-btn .seg-meta { font-size: 11px; color: var(--text-muted); letter-spacing: 0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.seg-btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* Secondary protocol selector — pill chips, wraps to multiple rows */
.protocol-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 4px 0 8px;
padding: 0;
min-width: 0;
}
.protocol-chip {
flex: 0 1 auto;
max-width: 100%;
min-width: 0;
background: transparent;
border: 1px solid var(--border);
border-radius: 999px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
.protocol-chip:hover:not(.active) {
background: var(--bg-subtle);
color: var(--text);
border-color: var(--border-strong);
}
.protocol-chip.active {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--accent-tint) 100%);
border-color: var(--accent);
color: var(--text);
font-weight: 600;
box-shadow: 0 0 0 2px var(--accent-soft);
}
.settings-section { display: flex; flex-direction: column; gap: 12px; }
.settings-rescan-btn {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 96px;
justify-content: center;
}
.settings-rescan-btn.loading {
border-color: var(--accent-soft);
background: var(--accent-tint);
color: var(--accent-strong);
}
.settings-rescan-status {
margin: -4px 0 0;
padding: 7px 10px;
border: 1px solid var(--green-border);
border-radius: var(--radius-sm);
background: var(--green-bg);
color: var(--green);
font-size: 12px;
line-height: 1.4;
}
.settings-rescan-status.error {
border-color: var(--red-border);
background: var(--red-bg);
color: var(--red);
}
.settings-field-error {
color: var(--red);
font-size: 12px;
line-height: 1.4;
}
.settings-about-list {
margin: 0;
@ -1289,6 +1362,10 @@ code {
align-items: flex-start;
gap: 12px;
}
.section-head > div {
max-width: 100%;
min-width: 0;
}
.section-head h3 { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: 0.01em; }
.section-head .hint { margin-top: 2px; }
.field { display: flex; flex-direction: column; gap: 4px; }

View file

@ -24,13 +24,17 @@ import type {
} from '../types';
import type { ArtifactManifest } from '../artifacts/types';
export async function fetchAgents(): Promise<AgentInfo[]> {
export async function fetchAgents(options?: { throwOnError?: boolean }): Promise<AgentInfo[]> {
try {
const resp = await fetch('/api/agents');
if (!resp.ok) return [];
if (!resp.ok) {
if (options?.throwOnError) throw new Error(`agents ${resp.status}`);
return [];
}
const json = (await resp.json()) as { agents: AgentInfo[] };
return json.agents ?? [];
} catch {
} catch (err) {
if (options?.throwOnError) throw err;
return [];
}
}

View file

@ -60,7 +60,7 @@ describe('loadConfig', () => {
expect(config.apiProtocol).toBe('anthropic');
});
it('does not migrate daemon-mode configs', () => {
it('infers protocol for legacy daemon-mode API fields without changing mode', () => {
const daemonConfig: Partial<AppConfig> = {
mode: 'daemon',
apiKey: 'sk-test',
@ -75,7 +75,7 @@ describe('loadConfig', () => {
const config = loadConfig();
expect(config.mode).toBe('daemon');
expect(config.apiProtocol).toBe('anthropic');
expect(config.apiProtocol).toBe('openai');
expect(config.configMigrationVersion).toBe(1);
});

View file

@ -50,6 +50,7 @@ export const DEFAULT_CONFIG: AppConfig = {
// saved baseUrl/model before applying the current migration version.
apiProtocol: 'anthropic',
apiVersion: '',
apiProtocolConfigs: {},
configMigrationVersion: CONFIG_MIGRATION_VERSION,
apiProviderBaseUrl: 'https://api.anthropic.com',
agentId: null,
@ -229,6 +230,7 @@ export function loadConfig(): AppConfig {
const merged: AppConfig = {
...DEFAULT_CONFIG,
...parsed,
apiProtocolConfigs: { ...(parsed.apiProtocolConfigs ?? {}) },
mediaProviders: { ...(parsed.mediaProviders ?? {}) },
agentModels: { ...(parsed.agentModels ?? {}) },
pet: normalizePet(parsed.pet),
@ -240,7 +242,7 @@ export function loadConfig(): AppConfig {
// protocol so old OpenAI-compatible endpoints keep routing correctly.
// This is version-gated instead of only field-gated so a later imported
// legacy config can be migrated when it is loaded.
if (!parsedHasApiProtocol && merged.mode === 'api') {
if (!parsedHasApiProtocol) {
merged.apiProtocol = inferApiProtocol(merged.model, merged.baseUrl);
// Also set apiProviderBaseUrl so setApiProtocol() can correctly identify
// whether the user is on a known provider and switch defaults appropriately.

View file

@ -43,6 +43,14 @@ export interface MediaProviderCredentials {
baseUrl: string;
}
export interface ApiProtocolConfig {
apiKey: string;
baseUrl: string;
model: string;
apiVersion?: string;
apiProviderBaseUrl?: string | null;
}
// Per-CLI model + reasoning the user picked in the model menu. Each agent
// keeps its own slot so flipping between Codex and Gemini doesn't reset the
// other one's choice. Missing entries fall back to the agent's first
@ -146,6 +154,7 @@ export interface AppConfig {
model: string;
apiProtocol?: ApiProtocol;
apiVersion?: string;
apiProtocolConfigs?: Partial<Record<ApiProtocol, ApiProtocolConfig>>;
/** Internal config schema/migration version for localStorage upgrades. */
configMigrationVersion?: number;
/** Base URL of the selected known provider; cleared once the user customizes provider fields. */

View file

@ -8,6 +8,10 @@ const AGENT_LABELS: Record<string, string> = {
cursor: 'Cursor',
qwen: 'Qwen',
copilot: 'Copilot',
'anthropic-api': 'Anthropic API',
'openai-api': 'OpenAI API',
'azure-openai-api': 'Azure OpenAI',
'google-gemini-api': 'Google Gemini',
};
const AGENT_ALIASES: Record<string, string> = {
@ -35,6 +39,24 @@ export function agentDisplayName(
return null;
}
export function exactAgentDisplayName(raw: string | null | undefined): string | null {
if (!raw) return null;
const key = normalizeKey(raw);
const alias = AGENT_ALIASES[key] ?? key;
return AGENT_LABELS[alias] ?? null;
}
export function agentModelDisplayName(
agentId?: string | null,
fallbackName?: string | null,
model?: string | null,
): string | undefined {
const label = agentDisplayName(agentId, fallbackName) ?? undefined;
const modelId = displayableModelId(model);
if (!modelId) return label;
return label ? `${label} · ${modelId}` : modelId;
}
function knownAgentLabel(raw: string | null | undefined): string | null {
if (!raw) return null;
const key = normalizeKey(raw);
@ -56,6 +78,12 @@ function safeFallbackLabel(raw: string | null | undefined): string | null {
return trimmed;
}
function displayableModelId(raw: string | null | undefined): string | null {
const trimmed = raw?.trim();
if (!trimmed || trimmed === 'default') return null;
return trimmed;
}
function normalizeKey(raw: string): string {
const basename = raw.trim().split(/[\\/]/).pop() ?? raw.trim();
return basename

View file

@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { apiProtocolLabel, apiProtocolModelLabel } from './apiProtocol';
import { agentModelDisplayName } from './agentLabels';
describe('api protocol labels', () => {
it('labels the selected API protocol instead of assuming Anthropic', () => {
expect(apiProtocolLabel('openai')).toBe('OpenAI API');
expect(apiProtocolLabel('google')).toBe('Google Gemini');
expect(apiProtocolLabel(undefined)).toBe('Anthropic API');
});
it('includes the selected model when labeling API assistant messages', () => {
expect(apiProtocolModelLabel('openai', 'google/gemma-4-e4b')).toBe(
'OpenAI API · google/gemma-4-e4b',
);
expect(apiProtocolModelLabel('azure', ' ')).toBe('Azure OpenAI');
});
it('includes explicit local CLI models when labeling agent messages', () => {
expect(agentModelDisplayName('claude', 'Claude Code', 'claude-sonnet-4-6')).toBe(
'Claude · claude-sonnet-4-6',
);
expect(agentModelDisplayName('claude', 'Claude Code', 'default')).toBe('Claude');
});
});

View file

@ -18,6 +18,15 @@ export function apiProtocolLabel(protocol: ApiProtocol | undefined): string {
return API_PROTOCOL_LABELS[protocol ?? 'anthropic'];
}
export function apiProtocolModelLabel(
protocol: ApiProtocol | undefined,
model: string,
): string {
const label = apiProtocolLabel(protocol);
const trimmed = model.trim();
return trimmed ? `${label} · ${trimmed}` : label;
}
export function apiProtocolAgentId(protocol: ApiProtocol | undefined): string {
return API_PROTOCOL_AGENT_IDS[protocol ?? 'anthropic'];
}