feat(claude-desktop): support custom model aliases
- Add editable model lists, alias suggestions, and usage guidance - Ignore unselected rows and validate alias conflicts and naming rules - Fix alias routing through disabled or excluded providers - Preserve model routes when restoring legacy backups - Repair legacy metadata and remove unsupported context fields - Add regression tests for validation and configuration recovery
This commit is contained in:
1 parent
300476043b
commit
81597fcf91
30 files changed
+2077
-193
No files matched your search
+28
-5
@@ -1085,8 +1085,31 @@ export const jaOverrides = {
|
||||
"agents.modify.moreDescription": "再構築やクリアは独自設定を削除します。必要な場合のみ使用してください。",
|
||||
"agents.modify.title": "{name} の設定を書き込む",
|
||||
'agents.modify.oauthConfiguration': 'OAuth ログイン設定方式',
|
||||
'agents.claudeDesktopMapping.title': 'Claude Desktop モデル → 上流モデル',
|
||||
'agents.claudeDesktopMapping.description': '役割ごとに上流モデルとコンテキスト機能を選択します。対応モデルでのみ 1M を有効にしてください。',
|
||||
'agents.claudeDesktopMapping.title': 'Claude Desktop モデルとエイリアス',
|
||||
'agents.claudeDesktopMapping.help': '使い方',
|
||||
'agents.claudeDesktopMapping.description': 'Claude Desktop では Claude 系列のモデルのみ使用できるため、他の系列のモデルには Claude 形式のエイリアスを設定する必要があります。元のモデル名がすでに Claude 系列なら、エイリアスの設定は不要です。空欄のままにしてください。',
|
||||
'agents.claudeDesktopMapping.collisionHint': 'エイリアスは、元モデル一覧にある既存のモデル ID と重複しない名前を推奨します。同じ名前があるとラウンドロビン方式でリクエストが別のモデルに振り分けられ、目的のモデルを呼び出せない場合があります。',
|
||||
'agents.claudeDesktopMapping.add': 'モデルを追加',
|
||||
'agents.claudeDesktopMapping.remove': 'モデルを削除',
|
||||
'agents.claudeDesktopMapping.model': '元モデル',
|
||||
'agents.claudeDesktopMapping.alias': 'エイリアス(Claude モデルは省略可)',
|
||||
'agents.claudeDesktopMapping.claudeDefault': 'Claude モデルは既定で元の ID を使用します',
|
||||
'agents.claudeDesktopMapping.sameModel': 'エイリアスが元モデルと同じです。設定は不要で、元の ID をそのまま使用します。',
|
||||
'agents.claudeDesktopMapping.defaultAliasExists': '既定のエイリアス「{alias}」はモデル一覧に存在します。別のモデルが呼び出されないよう、他のエイリアスに切り替えてください。',
|
||||
'agents.claudeDesktopMapping.noRename': '元の ID を使用',
|
||||
'agents.claudeDesktopMapping.suggestedAlias': 'エイリアス候補',
|
||||
'agents.claudeDesktopMapping.suggestionCount': '{count} 件のエイリアス候補',
|
||||
'agents.claudeDesktopMapping.customAliasHint': '一致する候補はありません。自由にエイリアスを入力できます。',
|
||||
'agents.model.useCustom': '入力したモデル ID を使用',
|
||||
'agents.claudeDesktopMapping.newModel': '新しいモデル',
|
||||
'agents.claudeDesktopMapping.idHint': 'エイリアスは claude-opus-*、claude-sonnet-*、claude-haiku-* などの形式にできます。ただし gpt・grok・gemini・deepseek など、他のモデル系列の名前は含められません。星印は任意の接尾辞を表します。そのまま入力せず、使いたい文字列に置き換えてください。',
|
||||
'agents.claudeDesktopMapping.empty': '「モデルを追加」で元モデルを入力してください。エイリアスは省略できます。',
|
||||
'agents.claudeDesktopMapping.error.empty': '元モデルを1つ以上選択または入力してください。',
|
||||
'agents.claudeDesktopMapping.error.model': '元モデルを選択または入力してください。',
|
||||
'agents.claudeDesktopMapping.error.modelFormat': '元モデル ID の形式が無効です。空白を削除するか、名前を短くしてください。',
|
||||
'agents.claudeDesktopMapping.error.aliasExists': 'このエイリアスは既存の元モデル ID と同じです。別のエイリアスを指定してください。',
|
||||
'agents.claudeDesktopMapping.error.alias': 'エイリアスは claude- で始まり、小文字の英数字・ハイフン・ピリオドのみ使用できます。gpt・grok・gemini・deepseek など他のモデル系列の名前は含められません。',
|
||||
'agents.claudeDesktopMapping.error.duplicate': 'エイリアスまたは元モデル ID が重複しています。各行に異なる ID を指定してください。',
|
||||
'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル',
|
||||
'agents.claudeCodeMapping.description': 'タスク役割ごとに上流モデルを選び、共通のコンテキストと自動圧縮ポリシーを設定します。',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': '最大コンテキスト',
|
||||
@@ -1098,9 +1121,9 @@ export const jaOverrides = {
|
||||
'agents.claudeCodeRuntime.disableAutoCompactHint': '有効時は /compact による手動圧縮のみ利用できます。',
|
||||
'agents.claudeDesktopMapping.customMapping': 'モデルエイリアスを使用',
|
||||
'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。',
|
||||
'agents.claudeDesktopMapping.opus': 'Claude Opus 5',
|
||||
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6',
|
||||
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5',
|
||||
'agents.claudeDesktopMapping.opus': 'Opus',
|
||||
'agents.claudeDesktopMapping.sonnet': 'Sonnet',
|
||||
'agents.claudeDesktopMapping.haiku': 'Haiku',
|
||||
'agents.claudeMapping.context1m': '1M',
|
||||
'agents.claudeMapping.context1mHint': '1M コンテキストとして設定します。選択した上流モデルが対応していることを確認してください。',
|
||||
'agents.oauthLoginRequired.title': 'Codex へのログインが必要です',
|
||||
|
||||
+28
-5
@@ -1051,8 +1051,31 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.notFetched': 'Not available',
|
||||
'agents.useModel': 'Model',
|
||||
'agents.pending': 'Not Applied',
|
||||
'agents.claudeDesktopMapping.title': 'Claude Desktop Model → Upstream Model',
|
||||
'agents.claudeDesktopMapping.description': 'Choose the upstream model and context capability for each role. Turn on 1M only for supported models.',
|
||||
'agents.claudeDesktopMapping.title': 'Claude Desktop Models and Aliases',
|
||||
'agents.claudeDesktopMapping.help': 'Usage guide',
|
||||
'agents.claudeDesktopMapping.description': 'Claude Desktop only allows Claude models, so non-Claude models need aliases that follow Claude model naming. If the original model name already belongs to the Claude family, no alias mapping is needed; leave the alias blank.',
|
||||
'agents.claudeDesktopMapping.collisionHint': 'Avoid aliases that match existing IDs in the original model list. Duplicate names can trigger round-robin scheduling, sending requests to another model instead of the one you intended.',
|
||||
'agents.claudeDesktopMapping.add': 'Add model',
|
||||
'agents.claudeDesktopMapping.remove': 'Remove model',
|
||||
'agents.claudeDesktopMapping.model': 'Original model',
|
||||
'agents.claudeDesktopMapping.alias': 'Alias (optional for Claude models)',
|
||||
'agents.claudeDesktopMapping.claudeDefault': 'Claude models use their original ID by default',
|
||||
'agents.claudeDesktopMapping.sameModel': 'The alias matches the original model. No alias is needed; the original ID will be used.',
|
||||
'agents.claudeDesktopMapping.defaultAliasExists': 'The default alias “{alias}” already exists in the model list. Choose another alias to avoid calling a different model.',
|
||||
'agents.claudeDesktopMapping.noRename': 'Keep original ID',
|
||||
'agents.claudeDesktopMapping.suggestedAlias': 'Suggested alias',
|
||||
'agents.claudeDesktopMapping.suggestionCount': '{count} suggested aliases',
|
||||
'agents.claudeDesktopMapping.customAliasHint': 'No matching suggestions. You can enter your own alias.',
|
||||
'agents.model.useCustom': 'Use the entered model ID',
|
||||
'agents.claudeDesktopMapping.newModel': 'New model',
|
||||
'agents.claudeDesktopMapping.idHint': 'Aliases can follow patterns such as claude-opus-*, claude-sonnet-*, or claude-haiku-*, but must not contain names of other model families such as gpt, grok, gemini, or deepseek. Replace the asterisk with your own suffix; do not enter it literally.',
|
||||
'agents.claudeDesktopMapping.empty': 'Click “Add model” and enter the original model. The alias is optional.',
|
||||
'agents.claudeDesktopMapping.error.empty': 'Choose or enter at least one original model.',
|
||||
'agents.claudeDesktopMapping.error.model': 'Choose or enter an original model.',
|
||||
'agents.claudeDesktopMapping.error.modelFormat': 'Invalid original model ID. Remove whitespace or shorten the name.',
|
||||
'agents.claudeDesktopMapping.error.aliasExists': 'This alias matches an existing original model ID. Choose a different alias.',
|
||||
'agents.claudeDesktopMapping.error.alias': 'Aliases must start with claude- and use lowercase letters, numbers, hyphens, and dots. Do not include other model families such as gpt, grok, gemini, or deepseek.',
|
||||
'agents.claudeDesktopMapping.error.duplicate': 'Duplicate alias or original model ID. Use a different ID for each row.',
|
||||
'agents.claudeCodeMapping.title': 'Claude Code Model → Upstream Model',
|
||||
'agents.claudeCodeMapping.description': 'Choose the upstream model for each task role and configure one shared context and auto-compact policy.',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': 'Maximum context',
|
||||
@@ -1064,9 +1087,9 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.claudeCodeRuntime.disableAutoCompactHint': 'Only manual /compact remains available when enabled.',
|
||||
'agents.claudeDesktopMapping.customMapping': 'Use Model Aliases',
|
||||
'agents.claudeDesktopMapping.customMappingHint': 'When enabled, only aliased models are shown; when disabled, only original models without aliases are shown.',
|
||||
'agents.claudeDesktopMapping.opus': 'Claude Opus 5',
|
||||
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6',
|
||||
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5',
|
||||
'agents.claudeDesktopMapping.opus': 'Opus',
|
||||
'agents.claudeDesktopMapping.sonnet': 'Sonnet',
|
||||
'agents.claudeDesktopMapping.haiku': 'Haiku',
|
||||
'agents.claudeMapping.context1m': '1M',
|
||||
'agents.claudeMapping.context1mHint': 'Configures a 1M context window. Make sure the selected upstream model supports it.',
|
||||
'agents.model.readingAvailable': 'Loading available models',
|
||||
|
||||
@@ -851,6 +851,7 @@ export const zhCN = {
|
||||
'agents.model.tryKeywords': '尝试其他关键词',
|
||||
'agents.model.connectFirst': '请先接入可用模型后刷新',
|
||||
'agents.model.available': '可用模型',
|
||||
'agents.model.useCustom': '使用输入的模型 ID',
|
||||
'agents.model.count': '{count} 个可用模型',
|
||||
'agents.model.stale': '刷新失败,显示上次结果',
|
||||
'agents.error.modelsLoading': '模型列表仍在加载,请稍后再试',
|
||||
@@ -1050,8 +1051,30 @@ export const zhCN = {
|
||||
'agents.notFetched': '未获取',
|
||||
'agents.useModel': '使用模型',
|
||||
'agents.pending': '尚未应用',
|
||||
'agents.claudeDesktopMapping.title': 'Claude Desktop 模型 → 上游模型',
|
||||
'agents.claudeDesktopMapping.description': '分别选择上游模型和上下文能力;仅为确认支持的模型打开 1M。',
|
||||
'agents.claudeDesktopMapping.title': 'Claude Desktop 模型与别名',
|
||||
'agents.claudeDesktopMapping.help': '使用说明',
|
||||
'agents.claudeDesktopMapping.description': 'Claude Desktop 只允许使用 Claude 系列模型,因此需要将非 Claude 系列模型映射为 Claude 系列的模型别名。如果原模型名称已经属于 Claude 系列,则无需映射,别名留空即可。',
|
||||
'agents.claudeDesktopMapping.collisionHint': '映射后的别名最好不要与原模型列表中已有的模型 ID 相同,以免产生轮询调度,导致请求被分配给其他模型,无法调用到期望的模型。',
|
||||
'agents.claudeDesktopMapping.add': '添加模型',
|
||||
'agents.claudeDesktopMapping.remove': '移除模型',
|
||||
'agents.claudeDesktopMapping.model': '原模型',
|
||||
'agents.claudeDesktopMapping.alias': '别名(Claude系列模型可留空)',
|
||||
'agents.claudeDesktopMapping.claudeDefault': 'Claude系列模型默认不映射',
|
||||
'agents.claudeDesktopMapping.sameModel': '别名与原模型相同,无需重复设置,将直接使用原模型 ID。',
|
||||
'agents.claudeDesktopMapping.defaultAliasExists': '默认别名「{alias}」已存在于模型列表中,请切换其他别名,避免调用到其他模型。',
|
||||
'agents.claudeDesktopMapping.noRename': '不重名',
|
||||
'agents.claudeDesktopMapping.suggestedAlias': '别名建议',
|
||||
'agents.claudeDesktopMapping.suggestionCount': '{count} 个别名建议',
|
||||
'agents.claudeDesktopMapping.customAliasHint': '没有匹配的建议,可自行填写别名。',
|
||||
'agents.claudeDesktopMapping.newModel': '新模型',
|
||||
'agents.claudeDesktopMapping.idHint': '别名可以采用 claude-opus-*、claude-sonnet-*、claude-haiku-* 这样的格式,但不能包含 gpt、grok、gemini、deepseek 等其他模型系列的名称。星号表示自定义后缀,不要原样输入。',
|
||||
'agents.claudeDesktopMapping.empty': '点击“添加模型”,填写原模型;别名可留空。',
|
||||
'agents.claudeDesktopMapping.error.empty': '请至少选择或填写一个原模型。',
|
||||
'agents.claudeDesktopMapping.error.model': '请选择或填写原模型。',
|
||||
'agents.claudeDesktopMapping.error.modelFormat': '原模型 ID 格式无效,请去掉空白字符或缩短名称。',
|
||||
'agents.claudeDesktopMapping.error.aliasExists': '该别名与已有原模型 ID 相同,请更换别名。',
|
||||
'agents.claudeDesktopMapping.error.alias': '别名须以 claude- 开头,仅使用小写字母、数字、连字符和小数点,且不能包含 gpt、grok、gemini、deepseek 等其他模型系列名称。',
|
||||
'agents.claudeDesktopMapping.error.duplicate': '别名或原模型 ID 重复,请为每一行使用不同的 ID。',
|
||||
'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型',
|
||||
'agents.claudeCodeMapping.description': '分别为不同任务角色选择上游模型,并统一配置上下文窗口与自动压缩。',
|
||||
'agents.claudeCodeRuntime.maxContextTokens': '最大窗口',
|
||||
@@ -1063,9 +1086,9 @@ export const zhCN = {
|
||||
'agents.claudeCodeRuntime.disableAutoCompactHint': '开启后仅允许用户通过 /compact 手动压缩。',
|
||||
'agents.claudeDesktopMapping.customMapping': '使用模型别名',
|
||||
'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。',
|
||||
'agents.claudeDesktopMapping.opus': 'Claude Opus 5',
|
||||
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6',
|
||||
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5',
|
||||
'agents.claudeDesktopMapping.opus': 'Opus',
|
||||
'agents.claudeDesktopMapping.sonnet': 'Sonnet',
|
||||
'agents.claudeDesktopMapping.haiku': 'Haiku',
|
||||
'agents.claudeMapping.context1m': '1M',
|
||||
'agents.claudeMapping.context1mHint': '打开后按 1M 上下文配置,请确认所选上游模型支持。',
|
||||
'agents.model.readingAvailable': '正在读取可用模型',
|
||||
|
||||
+217
-37
@@ -2,6 +2,7 @@ import { MessageNotice } from '../appNotice';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
type DeepSeekHarnessLaunchOptions,
|
||||
} from '../services/deepSeekHarnessLaunch';
|
||||
import type { ModelOption } from '../services/modelService';
|
||||
import { claudeDesktopAliasSuggestions, claudeDesktopDefaultAliases, createDefaultDesktopModels, desktopAliasNotice, desktopEntryValidation, desktopModelEntries, desktopModelId, desktopModelValidation, isClaudeDesktopModel, selectedDesktopModelEntries, type ClaudeDesktopModelMapping } from '../services/claudeDesktopModels';
|
||||
import { getCurrentLocale, translate, useI18n } from '../i18n';
|
||||
import { CodexSessionsPanel } from './CodexSessionsPanel';
|
||||
import { CodexModelCatalogDialog } from './CodexModelCatalogDialog';
|
||||
@@ -142,6 +144,7 @@ type PiProviderUpdateStatus = {
|
||||
type OAuthLoginRequiredAction = 'enable' | 'apply' | 'launch';
|
||||
|
||||
type ClaudeModelMappings = {
|
||||
desktopModels?: ClaudeDesktopModelMapping[];
|
||||
opus: string;
|
||||
sonnet: string;
|
||||
haiku: string;
|
||||
@@ -188,7 +191,7 @@ const createClaudeModelMappingsByClient = (): Record<
|
||||
ClaudeModelMappings
|
||||
> => ({
|
||||
'claude-code': createClaudeModelMappings(''),
|
||||
'claude-desktop': createClaudeModelMappings(''),
|
||||
'claude-desktop': { ...createClaudeModelMappings(''), desktopModels: createDefaultDesktopModels() },
|
||||
});
|
||||
|
||||
const createClaudeBooleanByClient = (): Record<ClaudeModelMappingClientId, boolean> => ({
|
||||
@@ -450,7 +453,9 @@ type AgentModelPickerProps = {
|
||||
error: string;
|
||||
disabled: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onRefresh: () => void;
|
||||
onRefresh?: () => void;
|
||||
allowCustomValue?: boolean;
|
||||
editable?: { label: string; placeholder: string; maxLength: number };
|
||||
};
|
||||
|
||||
type AgentModelDropdownLayout = {
|
||||
@@ -468,6 +473,8 @@ function AgentModelPicker({
|
||||
disabled,
|
||||
onChange,
|
||||
onRefresh,
|
||||
allowCustomValue = false,
|
||||
editable,
|
||||
}: AgentModelPickerProps) {
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -476,13 +483,21 @@ function AgentModelPicker({
|
||||
const [dropdownLayout, setDropdownLayout] = useState<AgentModelDropdownLayout | null>(null);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const visibleModels = useMemo(() => filterAgentModels(models, search), [models, search]);
|
||||
const choices = useMemo(
|
||||
() => visibleModels.map((model) => ({ name: model.name, alias: model.alias ?? '' })),
|
||||
[visibleModels],
|
||||
const valueRef = useRef<HTMLInputElement>(null);
|
||||
const listboxId = useId();
|
||||
const visibleModels = useMemo(
|
||||
() => editable && !search.trim() ? models : filterAgentModels(models, search),
|
||||
[models, search, editable],
|
||||
);
|
||||
const choices = useMemo(() => {
|
||||
const options = visibleModels.map((model) => ({ name: model.name, alias: model.alias ?? '' }));
|
||||
if (allowCustomValue && search.trim() && !findAgentModel(models, search)) {
|
||||
options.push({ name: search.trim(), alias: t('agents.model.useCustom') });
|
||||
}
|
||||
return options;
|
||||
}, [visibleModels, allowCustomValue, search, models, t]);
|
||||
const selectedModel = findAgentModel(models, value);
|
||||
const selectedName = selectedModel?.name ?? '';
|
||||
const selectedName = selectedModel?.name ?? (allowCustomValue || editable ? value.trim() : '');
|
||||
const selectedAlias = selectedName ? agentModelAlias(models, selectedName) : '';
|
||||
|
||||
const updateDropdownLayout = useCallback(() => {
|
||||
@@ -541,12 +556,12 @@ function AgentModelPicker({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setSearch('');
|
||||
const selectedIndex = filterAgentModels(models, '').findIndex(
|
||||
if (!editable) setSearch('');
|
||||
const selectedIndex = (editable ? visibleModels : filterAgentModels(models, '')).findIndex(
|
||||
(model) => model.name.toLocaleLowerCase() === value.trim().toLocaleLowerCase(),
|
||||
);
|
||||
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
||||
requestAnimationFrame(() => searchRef.current?.focus());
|
||||
if (!editable) requestAnimationFrame(() => searchRef.current?.focus());
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -556,6 +571,7 @@ function AgentModelPicker({
|
||||
const choose = (name: string) => {
|
||||
onChange(name);
|
||||
setOpen(false);
|
||||
if (editable) valueRef.current?.focus();
|
||||
};
|
||||
|
||||
const moveActive = (offset: number) => {
|
||||
@@ -566,11 +582,13 @@ function AgentModelPicker({
|
||||
const handleSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
moveActive(1);
|
||||
if (!open) setOpen(true);
|
||||
else moveActive(1);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
moveActive(-1);
|
||||
} else if (event.key === 'Enter' && choices[activeIndex]) {
|
||||
if (!open) setOpen(true);
|
||||
else moveActive(-1);
|
||||
} else if (event.key === 'Enter' && open && choices[activeIndex]) {
|
||||
event.preventDefault();
|
||||
choose(choices[activeIndex].name);
|
||||
} else if (event.key === 'Escape') {
|
||||
@@ -580,12 +598,35 @@ function AgentModelPicker({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`agent-model-picker ${open ? 'open' : ''}`} ref={rootRef}>
|
||||
<button
|
||||
<div className={`agent-model-picker ${open ? 'open' : ''}`} ref={rootRef}
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false);
|
||||
}}>
|
||||
{editable ? (
|
||||
<div className="agent-model-trigger agent-model-editable-trigger">
|
||||
<input ref={valueRef} type="text" value={value} aria-label={editable.label}
|
||||
placeholder={editable.placeholder} maxLength={editable.maxLength} disabled={disabled}
|
||||
spellCheck={false} autoComplete="off" role="combobox" aria-autocomplete="list"
|
||||
aria-controls={listboxId} aria-expanded={open}
|
||||
onClick={() => { setSearch(''); setOpen(true); }} onKeyDown={handleSearchKeyDown}
|
||||
onChange={(event) => {
|
||||
onChange(event.currentTarget.value);
|
||||
setSearch(event.currentTarget.value);
|
||||
setActiveIndex(0);
|
||||
setOpen(true);
|
||||
}} />
|
||||
<button type="button" className="icon-button quiet" aria-label={editable.label}
|
||||
aria-haspopup="listbox" aria-expanded={open} aria-controls={listboxId}
|
||||
disabled={disabled} onClick={() => { setSearch(''); setOpen((current) => !current); }}>
|
||||
<ChevronDown size={17} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
) : <button
|
||||
type="button"
|
||||
className="agent-model-trigger"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
aria-controls={listboxId}
|
||||
disabled={disabled}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
@@ -602,16 +643,16 @@ function AgentModelPicker({
|
||||
{selectedAlias ? <small title={selectedAlias}>{selectedAlias}</small> : null}
|
||||
</span>
|
||||
<ChevronDown size={17} aria-hidden />
|
||||
</button>
|
||||
</button>}
|
||||
|
||||
{open ? (
|
||||
<div
|
||||
className="agent-model-dropdown"
|
||||
className={`agent-model-dropdown${editable ? ' agent-model-dropdown-editable' : ''}`}
|
||||
style={dropdownLayout
|
||||
? dropdownLayout
|
||||
: { top: 0, left: 0, width: 0, height: 0, visibility: 'hidden' }}
|
||||
>
|
||||
<div className="agent-model-search">
|
||||
{!editable ? <div className="agent-model-search">
|
||||
<Search size={15} aria-hidden />
|
||||
<input
|
||||
ref={searchRef}
|
||||
@@ -623,7 +664,7 @@ function AgentModelPicker({
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder={t('agents.model.search')}
|
||||
role="combobox"
|
||||
aria-controls="agent-model-listbox"
|
||||
aria-controls={listboxId}
|
||||
aria-expanded="true"
|
||||
/>
|
||||
{search ? (
|
||||
@@ -640,20 +681,22 @@ function AgentModelPicker({
|
||||
<X size={14} />
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className="icon-button quiet" onClick={onRefresh} disabled={loading} title={t('agents.model.refresh')}>
|
||||
{onRefresh ? <button type="button" className="icon-button quiet" onClick={onRefresh} disabled={loading} title={t('agents.model.refresh')}>
|
||||
<RefreshCw size={14} className={loading ? 'spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</button> : null}
|
||||
</div> : null}
|
||||
|
||||
<div className="agent-model-list" id="agent-model-listbox" role="listbox">
|
||||
{loading && models.length === 0 ? (
|
||||
<div className="agent-model-list" id={listboxId} role="listbox">
|
||||
{loading && models.length === 0 && choices.length === 0 ? (
|
||||
<div className="agent-model-empty"><LoaderCircle size={18} className="spin" />{t('agents.model.fetching')}</div>
|
||||
) : error && models.length === 0 ? (
|
||||
) : error && models.length === 0 && choices.length === 0 ? (
|
||||
<div className="agent-model-empty error"><strong>{t('agents.model.loadFailed')}</strong><span>{error}</span></div>
|
||||
) : choices.length === 0 ? (
|
||||
<div className="agent-model-empty">
|
||||
<strong>{search.trim() ? t('agents.model.noMatch') : t('agents.model.unavailable')}</strong>
|
||||
<span>{search.trim() ? t('agents.model.tryKeywords') : t('agents.model.connectFirst')}</span>
|
||||
{editable ? <span>{t('agents.claudeDesktopMapping.customAliasHint')}</span> : <>
|
||||
<strong>{search.trim() ? t('agents.model.noMatch') : t('agents.model.unavailable')}</strong>
|
||||
<span>{search.trim() ? t('agents.model.tryKeywords') : t('agents.model.connectFirst')}</span>
|
||||
</>}
|
||||
</div>
|
||||
) : choices.map((choice, index) => {
|
||||
const selected = choice.name.toLocaleLowerCase() === value.trim().toLocaleLowerCase();
|
||||
@@ -677,7 +720,7 @@ function AgentModelPicker({
|
||||
})}
|
||||
</div>
|
||||
<div className="agent-model-dropdown-footer">
|
||||
<span>{t('agents.model.count', { count: models.length })}</span>
|
||||
<span>{t(editable ? 'agents.claudeDesktopMapping.suggestionCount' : 'agents.model.count', { count: models.length })}</span>
|
||||
{error && models.length > 0 ? <span className="error">{t('agents.model.stale')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -686,6 +729,43 @@ function AgentModelPicker({
|
||||
);
|
||||
}
|
||||
|
||||
function ClaudeDesktopHelpDialog({ onClose }: { onClose: () => void }) {
|
||||
const { t } = useI18n();
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const titleId = useId();
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
const previousFocus = document.activeElement;
|
||||
dialog?.showModal();
|
||||
return () => {
|
||||
dialog?.close();
|
||||
if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus();
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<dialog ref={dialogRef} className="config-dialog agent-desktop-help-dialog" aria-labelledby={titleId}
|
||||
onCancel={(event) => { event.preventDefault(); onClose(); }}
|
||||
onMouseDown={(event) => {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) onClose();
|
||||
}}>
|
||||
<div className="config-dialog-heading">
|
||||
<h2 id={titleId}>{t('agents.claudeDesktopMapping.help')}</h2>
|
||||
<button type="button" className="icon-button quiet" onClick={onClose} aria-label={t('common.close')}><X size={18} /></button>
|
||||
</div>
|
||||
<ol className="agent-desktop-help-list">
|
||||
<li>{t('agents.claudeDesktopMapping.description')}</li>
|
||||
<li>{t('agents.claudeDesktopMapping.collisionHint')}</li>
|
||||
<li>{t('agents.claudeDesktopMapping.idHint')}</li>
|
||||
</ol>
|
||||
<div className="agent-desktop-help-actions">
|
||||
<button type="button" className="primary-button" onClick={onClose}>{t('common.close')}</button>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
type AgentsPageProps = {
|
||||
embedded?: boolean;
|
||||
onConfigurationApplied?: () => void;
|
||||
@@ -753,6 +833,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
const [launchDirectoryDialogOpen, setLaunchDirectoryDialogOpen] = useState(false);
|
||||
const [codexCatalogDialogOpen, setCodexCatalogDialogOpen] = useState(false);
|
||||
const [harnessCatalogDialogOpen, setHarnessCatalogDialogOpen] = useState(false);
|
||||
const [desktopHelpOpen, setDesktopHelpOpen] = useState(false);
|
||||
const [launchDirectory, setLaunchDirectory] = useState('');
|
||||
const [launchDirectoryTarget, setLaunchDirectoryTarget] = useState<AgentLaunchTarget | null>(null);
|
||||
const [launchDirectoryError, setLaunchDirectoryError] = useState('');
|
||||
@@ -919,6 +1000,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
setClearError('');
|
||||
setClearConfirmOpen(false);
|
||||
setCodexCatalogDialogOpen(false);
|
||||
setDesktopHelpOpen(false);
|
||||
setLaunchDirectoryDialogOpen(false);
|
||||
setLaunchDirectoryTarget(null);
|
||||
setLaunchDirectoryError('');
|
||||
@@ -949,6 +1031,10 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
() => filterAgentModelsByAlias(models, claudeCustomMapping),
|
||||
[claudeCustomMapping, models],
|
||||
);
|
||||
const desktopEntries = claudeModelMappingsDraft.desktopModels ?? desktopModelEntries(claudeModelMappingsDraft);
|
||||
const appliedDesktopEntries = activeStatus?.claudeDesktopModelMappings
|
||||
? desktopModelEntries(activeStatus.claudeDesktopModelMappings) : [];
|
||||
const desktopValidation = desktopModelValidation(desktopEntries, models, appliedDesktopEntries);
|
||||
|
||||
const loadPiProviderUpdateStatus = useCallback(async () => {
|
||||
const requestId = piUpdateRequestRef.current + 1;
|
||||
@@ -980,12 +1066,12 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
: activeStatus?.pluginVersion ?? undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClaudeModelMappingClient || !selectedModel) return;
|
||||
if (!isClaudeModelMappingClient || (selected !== 'claude-desktop' && !selectedModel)) return;
|
||||
const appliedMappings = selected === 'claude-code'
|
||||
? activeStatus?.claudeCodeModelMappings
|
||||
: activeStatus?.claudeDesktopModelMappings;
|
||||
const dirty = claudeModelMappingsDirtyRef.current[selected];
|
||||
if (!dirty) {
|
||||
if (!dirty && selected === 'claude-code') {
|
||||
const appliedModels = appliedMappings
|
||||
? claudeMappingRoles
|
||||
.map((role) => findAgentModel(models, appliedMappings[role.key]))
|
||||
@@ -1007,6 +1093,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
dirty,
|
||||
);
|
||||
const next: ClaudeModelMappings = {
|
||||
...(selected === 'claude-desktop' ? { desktopModels: dirty && source.desktopModels
|
||||
? source.desktopModels : appliedMappings ? desktopModelEntries(source) : createDefaultDesktopModels() } : {}),
|
||||
opus: findAgentModel(models, source.opus)?.name ?? (selected === 'claude-desktop' ? '' : selectedModel),
|
||||
sonnet: findAgentModel(models, source.sonnet)?.name ?? (selected === 'claude-desktop' ? '' : selectedModel),
|
||||
haiku: findAgentModel(models, source.haiku)?.name ?? (selected === 'claude-desktop' ? '' : selectedModel),
|
||||
@@ -1067,9 +1155,9 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
});
|
||||
};
|
||||
const claudeMappingsReady = !isClaudeModelMappingClient
|
||||
|| claudeMappingRoles.every((role) =>
|
||||
|| (selected === 'claude-desktop' ? !desktopValidation : claudeMappingRoles.every((role) =>
|
||||
Boolean(findAgentModel(models, claudeModelMappingsDraft[role.key])),
|
||||
);
|
||||
));
|
||||
const claudeCodeRuntimeSettingsReady = selected !== 'claude-code' || (
|
||||
claudeModelMappingsDraft.maxContextTokens >= 100_000
|
||||
&& claudeModelMappingsDraft.maxContextTokens <= 1_000_000
|
||||
@@ -1250,6 +1338,15 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
|
||||
const requireClaudeModelMappings = (): ClaudeModelMappings | null => {
|
||||
if (!isClaudeModelMappingClient) return null;
|
||||
if (selected === 'claude-desktop') {
|
||||
if (desktopValidation) {
|
||||
setModelSelectionError(t(`agents.claudeDesktopMapping.error.${desktopValidation}`));
|
||||
return null;
|
||||
}
|
||||
const selectedEntries = selectedDesktopModelEntries(desktopEntries);
|
||||
return { ...createClaudeModelMappings(''), sonnet: selectedEntries[0].model.trim(),
|
||||
desktopModels: selectedEntries.map((entry) => ({ ...entry, model: entry.model.trim(), alias: entry.alias.trim() })) };
|
||||
}
|
||||
const resolved = {} as ClaudeModelMappings;
|
||||
for (const role of claudeMappingRoles) {
|
||||
const model = findAgentModel(models, claudeModelMappingsDraft[role.key]);
|
||||
@@ -1772,6 +1869,89 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
description={modificationDescription} />;
|
||||
const configurationErrorMessage = configurationError || (!nativeOauth ? modelSelectionError || modelError : '');
|
||||
|
||||
const editDesktopEntries = (entries: ClaudeDesktopModelMapping[]) => {
|
||||
setModelSelectionError('');
|
||||
editClaudeModelMappings((current) => ({ ...current, desktopModels: entries }));
|
||||
};
|
||||
const updateDesktopEntry = (index: number, changes: Partial<ClaudeDesktopModelMapping>) => {
|
||||
editDesktopEntries(desktopEntries.map((entry, i) => i === index ? { ...entry, ...changes } : entry));
|
||||
};
|
||||
const desktopModelEditor = (
|
||||
<section className="agent-core-setting-section agent-desktop-models">
|
||||
<div className="agent-section-heading">
|
||||
<strong>{t('agents.claudeDesktopMapping.title')}</strong>
|
||||
<div className="agent-section-heading-actions">
|
||||
<button type="button" className="secondary-button compact-button" onClick={() => setDesktopHelpOpen(true)}>
|
||||
{t('agents.claudeDesktopMapping.help')}
|
||||
</button>
|
||||
<button type="button" className="primary-button compact-button" disabled={busy || loading}
|
||||
onClick={() => editDesktopEntries([...desktopEntries, { model: '', alias: '', context1m: false }])}>
|
||||
{t('agents.claudeDesktopMapping.add')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!desktopEntries.length ? <p className="agent-model-hint">{t('agents.claudeDesktopMapping.empty')}</p> : null}
|
||||
<div className="agent-desktop-model-list">
|
||||
{desktopEntries.map((entry, index) => {
|
||||
const entryError = entry.model.trim() ? desktopEntryValidation(entry) : null;
|
||||
const aliasNotice = desktopAliasNotice(entry, desktopEntries, models, appliedDesktopEntries);
|
||||
const isClaude = isClaudeDesktopModel(entry.model);
|
||||
return (
|
||||
<div className="agent-claude-desktop-mapping-row agent-desktop-model-row" key={index}>
|
||||
<div className="agent-desktop-model-fields">
|
||||
<div className="agent-desktop-model-field">
|
||||
<span>{t('agents.claudeDesktopMapping.model')}</span>
|
||||
<AgentModelPicker models={models} value={entry.model} loading={modelLoading} error={modelError}
|
||||
allowCustomValue disabled={busy || loading || !activeStatus?.installed || !activeStatus.supportedPlatform}
|
||||
onChange={(model) => updateDesktopEntry(index, { model, ...(isClaudeDesktopModel(model) ? { alias: '' } : {}) })}
|
||||
onRefresh={refreshModels} />
|
||||
</div>
|
||||
<div className="agent-desktop-model-field">
|
||||
<span>{t('agents.claudeDesktopMapping.alias')}</span>
|
||||
<AgentModelPicker
|
||||
models={claudeDesktopAliasSuggestions.filter((alias) => !desktopEntries.some((other, i) => i !== index && other.model.trim() && desktopModelId(other).toLowerCase() === alias))
|
||||
.map((name) => ({ name, alias: t('agents.claudeDesktopMapping.suggestedAlias') }))}
|
||||
value={entry.alias} loading={false} error="" disabled={busy || loading}
|
||||
editable={{ label: t('agents.claudeDesktopMapping.alias'),
|
||||
placeholder: t(isClaude ? 'agents.claudeDesktopMapping.claudeDefault' : 'agents.claudeDesktopMapping.noRename'), maxLength: 128 }}
|
||||
onChange={(alias) => updateDesktopEntry(index, { alias })} />
|
||||
</div>
|
||||
<div className="agent-section-heading-actions agent-desktop-model-actions">
|
||||
<label className="agent-claude-context-toggle" title={t('agents.claudeMapping.context1mHint')}>
|
||||
<span>{t('agents.claudeMapping.context1m')}</span>
|
||||
<span className="switch-control"><input type="checkbox" checked={entry.context1m}
|
||||
onChange={(event) => updateDesktopEntry(index, { context1m: event.currentTarget.checked })}
|
||||
disabled={busy || loading} /><span className="switch-track" /></span>
|
||||
</label>
|
||||
<button type="button" className="icon-button quiet" aria-label={t('agents.claudeDesktopMapping.remove')}
|
||||
title={t('agents.claudeDesktopMapping.remove')} disabled={busy || loading}
|
||||
onClick={() => editDesktopEntries(desktopEntries.filter((_, i) => i !== index))}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{entryError && hasPendingChanges && !modelLoading
|
||||
? <p className="agent-inline-message warning agent-desktop-model-notice" role="status">{t(`agents.claudeDesktopMapping.error.${entryError}`)}</p>
|
||||
: aliasNotice === 'aliasExists'
|
||||
? <p className="agent-inline-message warning agent-desktop-model-notice" role="status">{t(
|
||||
claudeDesktopDefaultAliases.includes(entry.alias.trim().toLowerCase())
|
||||
? 'agents.claudeDesktopMapping.defaultAliasExists' : 'agents.claudeDesktopMapping.error.aliasExists',
|
||||
{ alias: entry.alias.trim() },
|
||||
)}</p>
|
||||
: aliasNotice === 'sameModel'
|
||||
? <p className="agent-desktop-model-notice" role="status">{t('agents.claudeDesktopMapping.sameModel')}</p>
|
||||
: isClaude
|
||||
? <p className="agent-desktop-model-notice" role="status">{t('agents.claudeDesktopMapping.claudeDefault')}</p>
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{desktopValidation === 'duplicate' && !modelLoading && hasPendingChanges
|
||||
? <p className="agent-inline-message warning" role="status">{t(`agents.claudeDesktopMapping.error.${desktopValidation}`)}</p> : null}
|
||||
</section>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={`page management-page agents-page${embedded ? ' agents-page-embedded' : ''}`}>
|
||||
<header className="management-header">
|
||||
@@ -1870,8 +2050,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
|
||||
<MessageNotice message={activeStatus?.error || activeStatus?.warnings.join(';')} tone={activeStatus?.error ? 'error' : 'info'} />
|
||||
|
||||
{selected === 'claude-desktop' && !activeStatus?.claudeDesktopModelMappings ? <p className="agent-inline-message warning">{t('agents.backup.mappingRequired')}</p> : null}
|
||||
<div className="agent-minimal-field">
|
||||
{selected === 'claude-desktop' ? desktopModelEditor : <div className="agent-minimal-field">
|
||||
<label htmlFor="embedded-agent-model">{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</label>
|
||||
<AgentModelPicker
|
||||
models={isClaudeModelMappingClient ? claudeMappingModels : models}
|
||||
@@ -1883,7 +2062,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
onRefresh={refreshModels}
|
||||
/>
|
||||
{codexCatalogButton}{harnessCatalogButton}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
|
||||
|
||||
@@ -2057,7 +2236,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{isClaudeModelMappingClient ? (
|
||||
{selected === 'claude-desktop' ? desktopModelEditor : null}
|
||||
{selected === 'claude-code' ? (
|
||||
<section className="agent-core-setting-section agent-claude-desktop-mapping">
|
||||
<div className="agent-section-heading">
|
||||
<div>
|
||||
@@ -2195,7 +2375,6 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
|
||||
<div className="agent-save-bar">
|
||||
{configurationFeedback}
|
||||
{selected === 'claude-desktop' && !activeStatus?.claudeDesktopModelMappings ? <p className="agent-inline-message warning">{t('agents.backup.mappingRequired')}</p> : null}
|
||||
<div className={`agent-save-actions${!isPiClient ? ' agent-codex-save-actions' : ''}`}>
|
||||
{closeConfigurationButton}
|
||||
<button type="button" className="primary-button" onClick={applySelectedConfiguration}
|
||||
@@ -2249,6 +2428,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{desktopHelpOpen ? <ClaudeDesktopHelpDialog onClose={() => setDesktopHelpOpen(false)} /> : null}
|
||||
{backupsOpen ? <AgentConfigBackupDialog client={selected} onClose={() => setBackupsOpen(false)} onRestored={async () => {
|
||||
clearPendingChanges();
|
||||
if (selected === 'claude-code' || selected === 'claude-desktop') claudeModelMappingsDirtyRef.current[selected] = false;
|
||||
|
||||
@@ -18,6 +18,7 @@ export type AgentConfigurationAction = 'apply' | 'update' | 'close';
|
||||
export type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop';
|
||||
|
||||
export type AgentModelMappings = {
|
||||
desktopModels?: import('./claudeDesktopModels').ClaudeDesktopModelMapping[];
|
||||
opus: string;
|
||||
sonnet: string;
|
||||
haiku: string;
|
||||
@@ -50,7 +51,15 @@ export const sameAgentModel = (left: string, right: string) => (
|
||||
export const sameAgentModelMappings = (
|
||||
left: AgentModelMappings,
|
||||
right: AgentModelMappings,
|
||||
) => sameAgentModel(left.opus, right.opus)
|
||||
) => (left.desktopModels !== undefined || right.desktopModels !== undefined)
|
||||
? left.desktopModels !== undefined && right.desktopModels !== undefined
|
||||
&& left.desktopModels.length === right.desktopModels.length
|
||||
&& left.desktopModels.every((entry, index) => {
|
||||
const other = right.desktopModels![index];
|
||||
return sameAgentModel(entry.model, other.model) && entry.alias.trim() === other.alias.trim()
|
||||
&& Boolean(entry.context1m) === Boolean(other.context1m);
|
||||
})
|
||||
: sameAgentModel(left.opus, right.opus)
|
||||
&& sameAgentModel(left.sonnet, right.sonnet)
|
||||
&& sameAgentModel(left.haiku, right.haiku)
|
||||
&& Boolean(left.opus1m) === Boolean(right.opus1m)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"otherModelFamilyPattern": "ark-code|astron|command-r|deepseek|doubao|gemini|gemma|glm|gpt|grok|hermes|hy3|kimi|lfm|\\bling\\b|llama|longcat|mimo|minimax|mistral|mixtral|moonshot|nemotron|openai|phi-|qianfan|qwen|tc-code|\\bunic\\b|yi-|stepfun|step-3|seed-|bytedance|hunyuan|granite|amazon\\.nova|nova-|devstral|ministral|ernie|codex|arcee|trinity|abab|phi\\d|\\bk2\\.|\\bm2\\.|jamba|arctic|solar|mercury|zamba|kat-coder|\\bds-|dpsk"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { AgentModelMappings } from './agentConfigurationDraft';
|
||||
import type { ModelOption } from './modelService';
|
||||
import claudeDesktopModelRules from './claudeDesktopModelRules.json';
|
||||
|
||||
export type ClaudeDesktopModelMapping = { model: string; alias: string; context1m: boolean };
|
||||
|
||||
export const desktopModelId = (entry: ClaudeDesktopModelMapping) => entry.alias.trim() || entry.model.trim();
|
||||
|
||||
export const claudeDesktopDefaultAliases = ['claude-opus-5', 'claude-sonnet-5', 'claude-haiku-4-5'];
|
||||
|
||||
export const createDefaultDesktopModels = (): ClaudeDesktopModelMapping[] =>
|
||||
claudeDesktopDefaultAliases.map((alias) => ({ model: '', alias, context1m: false }));
|
||||
|
||||
export const selectedDesktopModelEntries = (entries: ClaudeDesktopModelMapping[]) =>
|
||||
entries.filter((entry) => entry.model.trim());
|
||||
|
||||
export const claudeDesktopAliasSuggestions = [
|
||||
'claude-fable-5-1', 'claude-opus-5', 'claude-sonnet-5', 'claude-fable-5',
|
||||
'claude-opus-4-8', 'claude-opus-4-7',
|
||||
'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5',
|
||||
'claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-5-20251001',
|
||||
];
|
||||
|
||||
export const validClaudeDesktopAlias = (alias: string) => alias.trim().length <= 128
|
||||
&& /^claude-[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(alias.trim())
|
||||
&& isClaudeDesktopModel(alias);
|
||||
|
||||
const modelKey = (id: string) => id.trim().toLowerCase();
|
||||
|
||||
const otherModelFamily = new RegExp(claudeDesktopModelRules.otherModelFamilyPattern);
|
||||
|
||||
export function isClaudeDesktopModel(id: string) {
|
||||
const name = modelKey(id);
|
||||
return !/\s/.test(name) && !otherModelFamily.test(name)
|
||||
&& (name.startsWith('claude-') || /\banthropic[/.]/.test(name)
|
||||
|| /^(opus|sonnet|haiku|fable|mythos)(-[\d.]+)?$/.test(name));
|
||||
}
|
||||
|
||||
export function desktopEntryValidation(entry: ClaudeDesktopModelMapping) {
|
||||
const model = entry.model.trim();
|
||||
const alias = entry.alias.trim();
|
||||
if (!model) return 'model' as const;
|
||||
if (new TextEncoder().encode(model).length > 240 || /[\s\u0000-\u001f\u007f]/.test(model)) return 'modelFormat' as const;
|
||||
if (alias && modelKey(alias) !== modelKey(model) && !validClaudeDesktopAlias(alias)) return 'alias' as const;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function desktopAliasNotice(
|
||||
entry: ClaudeDesktopModelMapping,
|
||||
entries: ClaudeDesktopModelMapping[],
|
||||
models: ModelOption[],
|
||||
appliedEntries: ClaudeDesktopModelMapping[] = [],
|
||||
) {
|
||||
const alias = modelKey(entry.alias);
|
||||
if (!alias) return null;
|
||||
if (alias === modelKey(entry.model)) return 'sameModel' as const;
|
||||
const appliedAlias = appliedEntries.some((applied) => modelKey(applied.alias) === alias
|
||||
&& applied.model.trim() && modelKey(applied.model) !== alias);
|
||||
if (models.some((model) => modelKey(model.name) === alias && (!model.isAlias || !appliedAlias))
|
||||
|| entries.some((other) => modelKey(other.model) === alias)) return 'aliasExists' as const;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function desktopModelEntries(mappings: AgentModelMappings): ClaudeDesktopModelMapping[] {
|
||||
if (mappings.desktopModels) return mappings.desktopModels.map((entry) => {
|
||||
const model = entry.model.trim() || entry.alias.trim();
|
||||
return { ...entry, model, alias: modelKey(entry.alias) === modelKey(model) ? '' : entry.alias };
|
||||
});
|
||||
const aliases = ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
|
||||
const entries = (['opus', 'sonnet', 'haiku'] as const).flatMap((role, index) => {
|
||||
const model = mappings[role];
|
||||
if (!model) return [];
|
||||
return [{ model, alias: isClaudeDesktopModel(model) ? '' : aliases[index], context1m: Boolean(mappings[`${role}1m`]) }];
|
||||
});
|
||||
const unique: ClaudeDesktopModelMapping[] = [];
|
||||
for (const entry of entries) {
|
||||
const existing = unique.find((other) => desktopModelId(other) === desktopModelId(entry));
|
||||
if (existing) existing.context1m ||= entry.context1m;
|
||||
else unique.push(entry);
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
export function desktopModelValidation(
|
||||
entries: ClaudeDesktopModelMapping[],
|
||||
models: ModelOption[],
|
||||
appliedEntries: ClaudeDesktopModelMapping[] = [],
|
||||
) {
|
||||
const selectedEntries = selectedDesktopModelEntries(entries);
|
||||
if (!selectedEntries.length) return 'empty' as const;
|
||||
for (const entry of selectedEntries) {
|
||||
const error = desktopEntryValidation(entry);
|
||||
if (error) return error;
|
||||
}
|
||||
const aliases = selectedEntries.map((entry) => desktopModelId(entry).toLowerCase());
|
||||
if (new Set(aliases).size !== aliases.length) return 'duplicate' as const;
|
||||
if (selectedEntries.some((entry) => desktopAliasNotice(entry, selectedEntries, models, appliedEntries) === 'aliasExists')) return 'aliasExists' as const;
|
||||
return null;
|
||||
}
|
||||
+133
@@ -6827,6 +6827,98 @@ textarea:focus-visible {
|
||||
box-shadow: 0 1px 2px rgba(45, 42, 38, 0.04);
|
||||
}
|
||||
|
||||
.agent-desktop-models {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.agent-desktop-models > .agent-section-heading button {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-desktop-model-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-desktop-model-row {
|
||||
padding: 10px;
|
||||
border-radius: 7px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.agent-desktop-model-fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-desktop-model-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
font-size: var(--font-size-label);
|
||||
color: var(--theme-5f5a53);
|
||||
}
|
||||
|
||||
.agent-desktop-model-actions {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.agent-desktop-model-notice {
|
||||
margin: 0;
|
||||
color: var(--theme-8a847c);
|
||||
font-size: var(--font-size-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.agent-desktop-help-dialog {
|
||||
grid-template-rows: auto;
|
||||
align-content: start;
|
||||
width: min(600px, calc(100vw - 32px));
|
||||
height: fit-content;
|
||||
max-height: calc(100vh - 32px);
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.agent-desktop-help-dialog::backdrop {
|
||||
background: rgba(45, 42, 38, 0.42);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.agent-desktop-help-list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding-left: 22px;
|
||||
color: var(--theme-5f5a53);
|
||||
font-size: var(--font-size-body);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.agent-desktop-help-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.agent-desktop-models > .agent-section-heading { flex-wrap: wrap; }
|
||||
.agent-desktop-models > .agent-section-heading > .agent-section-heading-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.agent-desktop-model-fields { grid-template-columns: minmax(0, 1fr); gap: 8px; }
|
||||
.agent-desktop-model-actions { height: 34px; justify-self: end; }
|
||||
}
|
||||
|
||||
.agent-claude-desktop-mapping-row:has(.agent-claude-context-toggle input:checked) {
|
||||
border-color: rgba(128, 105, 173, 0.48);
|
||||
box-shadow: 0 0 0 2px rgba(128, 105, 173, 0.08);
|
||||
@@ -6977,6 +7069,43 @@ textarea:focus-visible {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.agent-model-editable-trigger input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: var(--font-size-body);
|
||||
}
|
||||
|
||||
.agent-model-editable-trigger:focus-within {
|
||||
border-color: var(--theme-8069ad);
|
||||
box-shadow: 0 0 0 2px rgba(128, 105, 173, 0.14);
|
||||
}
|
||||
|
||||
.agent-model-editable-trigger:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
background: var(--theme-f4f1ea);
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.agent-model-editable-trigger > button {
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--theme-8a847c);
|
||||
}
|
||||
|
||||
.agent-model-picker.open .agent-model-editable-trigger > button > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.agent-model-trigger strong,
|
||||
.agent-model-trigger small {
|
||||
overflow: hidden;
|
||||
@@ -7016,6 +7145,10 @@ textarea:focus-visible {
|
||||
box-shadow: 0 14px 34px rgba(45, 42, 38, 0.2);
|
||||
}
|
||||
|
||||
.agent-model-dropdown-editable {
|
||||
grid-template-rows: minmax(0, 1fr) 28px;
|
||||
}
|
||||
|
||||
.agent-model-search {
|
||||
display: grid;
|
||||
grid-template-columns: 17px minmax(0, 1fr) auto auto;
|
||||
|
||||
Reference in new issue
Block a user