diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index b3cf866..ef7f5a9 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -141,6 +141,10 @@ const createClaudeBooleanByClient = (): Record>>( readAgentModelSelections, ); - const [claudeModelMappingsDraftByClient, setClaudeModelMappingsDraftByClient] = useState( - createClaudeModelMappingsByClient, + const [claudeModelMappingsDraftByClient, setClaudeModelMappingsDraftByClientState] = useState( + () => ({ + 'claude-code': { ...claudeModelMappingsDraftCache['claude-code'] }, + 'claude-desktop': { ...claudeModelMappingsDraftCache['claude-desktop'] }, + }), ); - const [claudeCustomMappingByClient, setClaudeCustomMappingByClient] = useState( - createClaudeBooleanByClient, + const [claudeCustomMappingByClient, setClaudeCustomMappingByClientState] = useState( + () => ({ ...claudeCustomMappingCache }), ); const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); @@ -589,7 +596,31 @@ export function AgentsPage() { const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState(null); const modelRequestRef = useRef(0); const piUpdateRequestRef = useRef(0); - const claudeModelMappingsDirtyRef = useRef(createClaudeBooleanByClient()); + const claudeModelMappingsDirtyRef = useRef(claudeModelMappingsDirtyCache); + + const setClaudeModelMappingsDraftByClient = useCallback(( + update: ( + current: Record, + ) => Record, + ) => { + setClaudeModelMappingsDraftByClientState((current) => { + const next = update(current); + claudeModelMappingsDraftCache = next; + return next; + }); + }, []); + + const setClaudeCustomMappingByClient = useCallback(( + update: ( + current: Record, + ) => Record, + ) => { + setClaudeCustomMappingByClientState((current) => { + const next = update(current); + claudeCustomMappingCache = next; + return next; + }); + }, []); const loadStatuses = useCallback(async (forceRefresh = false) => { const command = forceRefresh @@ -685,9 +716,8 @@ export function AgentsPage() { setClearConfirmOpen(false); setOauthLoginRequiredAction(null); setOauthConfigurationDraft(null); - if (selected === 'claude-code' || selected === 'claude-desktop') { - claudeModelMappingsDirtyRef.current[selected] = false; - } + // Preserve each Claude client's unsaved draft while navigating between clients. + // Its dirty flag is cleared only after a successful apply, close, or reset action. }, [selected]); const activeDefinition = agentDefinitions.find((agent) => agent.id === selected) diff --git a/tests/agentModelPicker.test.ts b/tests/agentModelPicker.test.ts index 2fad359..8f984ae 100644 --- a/tests/agentModelPicker.test.ts +++ b/tests/agentModelPicker.test.ts @@ -100,7 +100,7 @@ const appliedConfiguration = ( }); describe('agent configuration update action', () => { - test('Claude mapping clients do not inherit another client draft', () => { + test('Claude mapping clients keep their own unsaved draft after switching away and back', () => { const codeDraft = { opus: 'code-opus', sonnet: 'code-sonnet', haiku: 'code-haiku' }; const desktopDraft = { opus: 'desktop-opus', @@ -116,26 +116,27 @@ describe('agent configuration update action', () => { 'claude-code': codeDraft, 'claude-desktop': desktopDraft, }; + const dirtyByClient = { + 'claude-code': true, + 'claude-desktop': false, + }; + + // Switch from Claude Code to Desktop: Desktop still loads its own default. expect(resolveAgentModelMappingsDraftSourceForClient( drafts, 'claude-desktop', null, desktopFallback, - false, + dirtyByClient['claude-desktop'], )).toEqual(desktopFallback); - expect(resolveAgentModelMappingsDraftSourceForClient( - drafts, - 'claude-desktop', - null, - desktopFallback, - true, - )).toEqual(desktopDraft); + + // Switch back to Claude Code: its unapplied selection must not be overwritten. expect(resolveAgentModelMappingsDraftSourceForClient( drafts, 'claude-code', - null, + { opus: 'applied', sonnet: 'applied', haiku: 'applied' }, desktopFallback, - true, + dirtyByClient['claude-code'], )).toEqual(codeDraft); });