From 476d6e6769aded9ff0d04336133575fb1759af5f Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 11 Aug 2026 14:12:16 +0800 Subject: [PATCH] fix(agents): isolate Claude Code and Desktop mapping state --- src/pages/AgentsPage.tsx | 136 ++++++++++++++++-------- src/services/agentConfigurationDraft.ts | 15 +++ tests/agentModelPicker.test.ts | 28 ++++- 3 files changed, 132 insertions(+), 47 deletions(-) diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index d008e74..b3cf866 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -39,7 +39,7 @@ import { } from '../services/agentModelPicker'; import { resolveAgentConfigurationAction, - resolveAgentModelMappingsDraftSource, + resolveAgentModelMappingsDraftSourceForClient, sameAgentModel, sameAgentModelMappings, } from '../services/agentConfigurationDraft'; @@ -56,6 +56,8 @@ type AgentClientId = | 'hermes' | 'pi'; +type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop'; + type AgentModificationState = 'unconfigured' | 'applied' | 'invalid'; type AgentConfigStatus = { @@ -126,6 +128,19 @@ const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({ disableAutoCompact: false, }); +const createClaudeModelMappingsByClient = (): Record< + ClaudeModelMappingClientId, + ClaudeModelMappings +> => ({ + 'claude-code': createClaudeModelMappings(''), + 'claude-desktop': createClaudeModelMappings(''), +}); + +const createClaudeBooleanByClient = (): Record => ({ + 'claude-code': false, + 'claude-desktop': false, +}); + const claudeMappingRoles = [ { key: 'opus', @@ -548,10 +563,12 @@ export function AgentsPage() { const [modelByClient, setModelByClient] = useState>>( readAgentModelSelections, ); - const [claudeModelMappingsDraft, setClaudeModelMappingsDraft] = useState( - createClaudeModelMappings(''), + const [claudeModelMappingsDraftByClient, setClaudeModelMappingsDraftByClient] = useState( + createClaudeModelMappingsByClient, + ); + const [claudeCustomMappingByClient, setClaudeCustomMappingByClient] = useState( + createClaudeBooleanByClient, ); - const [claudeCustomMapping, setClaudeCustomMapping] = useState(false); const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); const [busyAction, setBusyAction] = useState< @@ -572,7 +589,7 @@ export function AgentsPage() { const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState(null); const modelRequestRef = useRef(0); const piUpdateRequestRef = useRef(0); - const claudeModelMappingsDirtyRef = useRef(false); + const claudeModelMappingsDirtyRef = useRef(createClaudeBooleanByClient()); const loadStatuses = useCallback(async (forceRefresh = false) => { const command = forceRefresh @@ -668,7 +685,9 @@ export function AgentsPage() { setClearConfirmOpen(false); setOauthLoginRequiredAction(null); setOauthConfigurationDraft(null); - claudeModelMappingsDirtyRef.current = false; + if (selected === 'claude-code' || selected === 'claude-desktop') { + claudeModelMappingsDirtyRef.current[selected] = false; + } }, [selected]); const activeDefinition = agentDefinitions.find((agent) => agent.id === selected) @@ -682,6 +701,12 @@ export function AgentsPage() { const selectedModel = selectedModelOption?.name ?? ''; const isPiClient = selected === 'pi'; const isClaudeModelMappingClient = selected === 'claude-code' || selected === 'claude-desktop'; + const claudeModelMappingsDraft = isClaudeModelMappingClient + ? claudeModelMappingsDraftByClient[selected] + : createClaudeModelMappings(''); + const claudeCustomMapping = isClaudeModelMappingClient + ? claudeCustomMappingByClient[selected] + : false; const claudeMappingModels = useMemo( () => filterAgentModelsByAlias(models, claudeCustomMapping), [claudeCustomMapping, models], @@ -721,23 +746,27 @@ export function AgentsPage() { const appliedMappings = selected === 'claude-code' ? activeStatus?.claudeCodeModelMappings : activeStatus?.claudeDesktopModelMappings; - if (!claudeModelMappingsDirtyRef.current) { + const dirty = claudeModelMappingsDirtyRef.current[selected]; + if (!dirty) { const appliedModels = appliedMappings ? claudeMappingRoles .map((role) => findAgentModel(models, appliedMappings[role.key])) .filter((model): model is ModelOption => model !== null) : []; - setClaudeCustomMapping( - appliedModels.length === claudeMappingRoles.length + setClaudeCustomMappingByClient((current) => ({ + ...current, + [selected]: appliedModels.length === claudeMappingRoles.length && appliedModels.every((model) => Boolean(model.isAlias)), - ); + })); } - setClaudeModelMappingsDraft((current) => { - const source = resolveAgentModelMappingsDraftSource( + setClaudeModelMappingsDraftByClient((current) => { + const currentClientDraft = current[selected]; + const source = resolveAgentModelMappingsDraftSourceForClient( current, + selected, appliedMappings, createClaudeModelMappings(selectedModel), - claudeModelMappingsDirtyRef.current, + dirty, ); const next: ClaudeModelMappings = { opus: findAgentModel(models, source.opus)?.name ?? selectedModel, @@ -750,7 +779,9 @@ export function AgentsPage() { autoCompactPct: source.autoCompactPct ?? DEFAULT_CLAUDE_AUTO_COMPACT_PCT, disableAutoCompact: Boolean(source.disableAutoCompact), }; - return sameAgentModelMappings(current, next) ? current : next; + return sameAgentModelMappings(currentClientDraft, next) + ? current + : { ...current, [selected]: next }; }); }, [ activeStatus?.claudeCodeModelMappings, @@ -849,26 +880,30 @@ export function AgentsPage() { value: string, ) => { const model = findAgentModel(models, value); - if (!model) return; - claudeModelMappingsDirtyRef.current = true; + if (!model || !isClaudeModelMappingClient) return; + claudeModelMappingsDirtyRef.current[selected] = true; setModelSelectionError(''); - setClaudeModelMappingsDraft((current) => ({ ...current, [role]: model.name })); + setClaudeModelMappingsDraftByClient((current) => ({ + ...current, + [selected]: { ...current[selected], [role]: model.name }, + })); }; const changeClaude1mPreference = ( preference: 'opus1m' | 'sonnet1m' | 'haiku1m', enabled: boolean, ) => { - claudeModelMappingsDirtyRef.current = true; - setClaudeModelMappingsDraft((current) => { - const next = { ...current, [preference]: enabled }; + if (!isClaudeModelMappingClient) return; + claudeModelMappingsDirtyRef.current[selected] = true; + setClaudeModelMappingsDraftByClient((current) => { + const next = { ...current[selected], [preference]: enabled }; if (selected === 'claude-code') { const any1mEnabled = next.opus1m || next.sonnet1m || next.haiku1m; next.maxContextTokens = any1mEnabled ? 1_000_000 : DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS; } - return next; + return { ...current, [selected]: next }; }); }; @@ -876,37 +911,47 @@ export function AgentsPage() { key: 'maxContextTokens' | 'autoCompactPct', value: number, ) => { - claudeModelMappingsDirtyRef.current = true; - setClaudeModelMappingsDraft((current) => ({ ...current, [key]: value })); + if (selected !== 'claude-code') return; + claudeModelMappingsDirtyRef.current[selected] = true; + setClaudeModelMappingsDraftByClient((current) => ({ + ...current, + [selected]: { ...current[selected], [key]: value }, + })); }; const changeClaudeCodeAutoCompactDisabled = (disabled: boolean) => { - claudeModelMappingsDirtyRef.current = true; - setClaudeModelMappingsDraft((current) => ({ + if (selected !== 'claude-code') return; + claudeModelMappingsDirtyRef.current[selected] = true; + setClaudeModelMappingsDraftByClient((current) => ({ ...current, - disableAutoCompact: disabled, + [selected]: { + ...current[selected], + disableAutoCompact: disabled, + }, })); }; const changeClaudeCustomMapping = (enabled: boolean) => { - setClaudeCustomMapping(enabled); + if (!isClaudeModelMappingClient) return; + setClaudeCustomMappingByClient((current) => ({ ...current, [selected]: enabled })); setModelSelectionError(''); - setClaudeModelMappingsDraft((current) => { + setClaudeModelMappingsDraftByClient((current) => { + const currentClientDraft = current[selected]; const next: ClaudeModelMappings = { - opus: resolveAgentModelForAliasMode(models, current.opus, enabled), - sonnet: resolveAgentModelForAliasMode(models, current.sonnet, enabled), - haiku: resolveAgentModelForAliasMode(models, current.haiku, enabled), - opus1m: current.opus1m, - sonnet1m: current.sonnet1m, - haiku1m: current.haiku1m, - maxContextTokens: current.maxContextTokens, - autoCompactPct: current.autoCompactPct, - disableAutoCompact: current.disableAutoCompact, + opus: resolveAgentModelForAliasMode(models, currentClientDraft.opus, enabled), + sonnet: resolveAgentModelForAliasMode(models, currentClientDraft.sonnet, enabled), + haiku: resolveAgentModelForAliasMode(models, currentClientDraft.haiku, enabled), + opus1m: currentClientDraft.opus1m, + sonnet1m: currentClientDraft.sonnet1m, + haiku1m: currentClientDraft.haiku1m, + maxContextTokens: currentClientDraft.maxContextTokens, + autoCompactPct: currentClientDraft.autoCompactPct, + disableAutoCompact: currentClientDraft.disableAutoCompact, }; - if (!sameAgentModelMappings(current, next)) { - claudeModelMappingsDirtyRef.current = true; + if (!sameAgentModelMappings(currentClientDraft, next)) { + claudeModelMappingsDirtyRef.current[selected] = true; } - return next; + return { ...current, [selected]: next }; }); }; @@ -997,9 +1042,11 @@ export function AgentsPage() { claudeCodeModelMappings: selected === 'claude-code' ? claudeModelMappings : null, claudeDesktopModelMappings: selected === 'claude-desktop' ? claudeModelMappings : null, }); + if (isClaudeModelMappingClient) { + claudeModelMappingsDirtyRef.current[selected] = false; + } await reloadStatusesAfterAction(); setOauthConfigurationDraft(null); - claudeModelMappingsDirtyRef.current = false; } catch (requestError) { if (!handleOAuthLoginError(requestError, 'apply')) { setConfigurationError(String(requestError)); @@ -1075,6 +1122,9 @@ export function AgentsPage() { setBusyAction('close-config'); try { await invoke('close_agent_config_modification', { client: selected }); + if (isClaudeModelMappingClient) { + claudeModelMappingsDirtyRef.current[selected] = false; + } await reloadStatusesAfterAction(); setOauthConfigurationDraft(null); } catch (requestError) { @@ -1102,9 +1152,11 @@ export function AgentsPage() { claudeDesktopModelMappings: selected === 'claude-desktop' ? claudeModelMappings : null, }); setDefaultConfirmOpen(false); + if (isClaudeModelMappingClient) { + claudeModelMappingsDirtyRef.current[selected] = false; + } await reloadStatusesAfterAction(); setOauthConfigurationDraft(null); - claudeModelMappingsDirtyRef.current = false; } catch (requestError) { if (!handleOAuthLoginError(requestError, 'apply')) { setDefaultError(String(requestError)); diff --git a/src/services/agentConfigurationDraft.ts b/src/services/agentConfigurationDraft.ts index ad7c5a9..18086a9 100644 --- a/src/services/agentConfigurationDraft.ts +++ b/src/services/agentConfigurationDraft.ts @@ -11,6 +11,8 @@ export type AgentConfigurationModificationState = 'unconfigured' | 'applied' | ' export type AgentConfigurationAction = 'apply' | 'update' | 'close'; +export type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop'; + export type AgentModelMappings = { opus: string; sonnet: string; @@ -60,6 +62,19 @@ export const resolveAgentModelMappingsDraftSource = ( dirty: boolean, ): T => (dirty ? current : applied ?? fallback); +export const resolveAgentModelMappingsDraftSourceForClient = ( + currentByClient: Record, + client: ClaudeModelMappingClientId, + applied: T | null | undefined, + fallback: T, + dirty: boolean, +): T => resolveAgentModelMappingsDraftSource( + currentByClient[client], + applied, + fallback, + dirty, +); + export function resolveAgentConfigurationAction({ client, modificationState, diff --git a/tests/agentModelPicker.test.ts b/tests/agentModelPicker.test.ts index 0476abd..2fad359 100644 --- a/tests/agentModelPicker.test.ts +++ b/tests/agentModelPicker.test.ts @@ -10,7 +10,7 @@ import { } from '../src/services/agentModelPicker'; import { resolveAgentConfigurationAction, - resolveAgentModelMappingsDraftSource, + resolveAgentModelMappingsDraftSourceForClient, sameAgentModelMappings, type AgentConfigurationClientId, } from '../src/services/agentConfigurationDraft'; @@ -102,19 +102,37 @@ const appliedConfiguration = ( describe('agent configuration update action', () => { test('Claude mapping clients do not inherit another client draft', () => { const codeDraft = { opus: 'code-opus', sonnet: 'code-sonnet', haiku: 'code-haiku' }; + const desktopDraft = { + opus: 'desktop-opus', + sonnet: 'desktop-sonnet', + haiku: 'desktop-haiku', + }; const desktopFallback = { opus: 'desktop-model', sonnet: 'desktop-model', haiku: 'desktop-model', }; - expect(resolveAgentModelMappingsDraftSource( - codeDraft, + const drafts = { + 'claude-code': codeDraft, + 'claude-desktop': desktopDraft, + }; + expect(resolveAgentModelMappingsDraftSourceForClient( + drafts, + 'claude-desktop', null, desktopFallback, false, )).toEqual(desktopFallback); - expect(resolveAgentModelMappingsDraftSource( - codeDraft, + expect(resolveAgentModelMappingsDraftSourceForClient( + drafts, + 'claude-desktop', + null, + desktopFallback, + true, + )).toEqual(desktopDraft); + expect(resolveAgentModelMappingsDraftSourceForClient( + drafts, + 'claude-code', null, desktopFallback, true,