fix(agents): isolate Claude Code and Desktop mapping state

This commit is contained in:
lzt404 committed 2026-08-11 14:12:16 +08:00
1 parent 4484e3a190
commit 476d6e6769
3 files changed
+132 -47

No files matched your search

+94 -42
View File
@@ -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<ClaudeModelMappingClientId, boolean> => ({
'claude-code': false,
'claude-desktop': false,
});
const claudeMappingRoles = [
{
key: 'opus',
@@ -548,10 +563,12 @@ export function AgentsPage() {
const [modelByClient, setModelByClient] = useState<Partial<Record<AgentClientId, string>>>(
readAgentModelSelections,
);
const [claudeModelMappingsDraft, setClaudeModelMappingsDraft] = useState<ClaudeModelMappings>(
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<PiProviderUpdateStatus | null>(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<AgentConfigActionResult>('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));
+15
View File
@@ -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 = <T>(
dirty: boolean,
): T => (dirty ? current : applied ?? fallback);
export const resolveAgentModelMappingsDraftSourceForClient = <T>(
currentByClient: Record<ClaudeModelMappingClientId, T>,
client: ClaudeModelMappingClientId,
applied: T | null | undefined,
fallback: T,
dirty: boolean,
): T => resolveAgentModelMappingsDraftSource(
currentByClient[client],
applied,
fallback,
dirty,
);
export function resolveAgentConfigurationAction({
client,
modificationState,
+23 -5
View File
@@ -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,