File size: 6,155 Bytes
3d700dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { useMutation, useQueryClient } from "@tanstack/react-query";
import AgentServerConversationService from "#/api/conversation-service/agent-server-conversation-service.api";
import AgentProfilesService, {
  type AgentProfileListResponse,
} from "#/api/agent-profiles-service/agent-profiles-service.api";
import SettingsService from "#/api/settings-service/settings-service.api";
import { mergeAgentProfileSaveInput } from "#/components/features/settings/agent-profiles/merge-agent-profile-save-input";
import { useActiveBackend } from "#/contexts/active-backend-context";
import {
  AGENT_PROFILES_QUERY_KEYS,
  AGENT_PROFILES_RETRY_OPTIONS,
  SETTINGS_QUERY_KEYS,
} from "#/hooks/query/query-keys";
import { agentProfileDetailQueryKey } from "#/hooks/query/use-active-acp-profile-detail";
import { invalidateConversationQueries } from "./conversation-mutation-utils";

interface SwitchAcpModelVars {
  /**
   * When set, the ACP conversation's running model is swapped live via the
   * wrapper's ``session/set_model`` (POST /switch_acp_model) and the user's
   * saved default is untouched. When null (home page / no session), the model
   * is persisted as the default the next conversation launches with.
   */
  conversationId: string | null;
  model: string;
}

/**
 * ACP analog of {@link useSwitchLlmProfile}. Switches the ACP model
 * per-conversation when called from inside a conversation (live in-place model
 * switch; a created-but-not-yet-run conversation is a persist-only deferral on
 * the server, so blank conversations work too); persists it as the launch
 * default when called from the home page.
 *
 * The home-page persist target is the **active ACP AgentProfile**: profile
 * launches resolve the agent server-side purely from the stored profile
 * (``agent_profile_id`` and ``agent_settings`` are mutually exclusive launch
 * sources), so writing ``agent_settings.acp_model`` would not affect them.
 * Only when the profile list resolves with no active ACP profile does the
 * write fall back to the ``agent_settings_diff`` the legacy launch path
 * reads. A failed profile discovery propagates instead of downgrading
 * (#16523): an active-profile launch ignores agent_settings, so persisting
 * the pick there would silently drop it.
 *
 * Invalidates the same conversation/settings query keys the profile hook does
 * so the chat-input model chip + conversation chip refresh with the new model.
 */
export const useSwitchAcpModel = () => {
  const queryClient = useQueryClient();
  const { backend, orgId } = useActiveBackend();

  return useMutation({
    mutationFn: async ({ conversationId, model }: SwitchAcpModelVars) => {
      if (conversationId) {
        await AgentServerConversationService.switchAcpModel(
          conversationId,
          model,
        );
        return;
      }

      // Home page: resolve the active profile from the shared query cache
      // (same keys/retry policy as useCreateConversation's launch path, so a
      // pick can't disagree with what a subsequent send would launch). Like
      // that launch path, do not fall back to the legacy agent_settings
      // persist when profile discovery fails (#16523): an active-profile
      // launch ignores agent_settings, so the pick would be silently dropped.
      // The rejection surfaces via the global mutation error toast.
      const agentProfiles: AgentProfileListResponse =
        await queryClient.ensureQueryData({
          queryKey: [...AGENT_PROFILES_QUERY_KEYS.all, backend.id, orgId],
          queryFn: AgentProfilesService.listProfiles,
          ...AGENT_PROFILES_RETRY_OPTIONS,
        });
      const activeProfile = agentProfiles.profiles?.find(
        (profile) =>
          profile.id != null &&
          profile.id === agentProfiles.active_agent_profile_id,
      );

      if (activeProfile?.agent_kind === "acp") {
        // Merge over the stored profile so a model-only pick can't wipe the
        // fields this surface doesn't model (command, session mode, …).
        const detail = await queryClient.ensureQueryData({
          queryKey: agentProfileDetailQueryKey(
            backend.id,
            orgId,
            activeProfile.name,
          ),
          queryFn: () => AgentProfilesService.getProfile(activeProfile.name),
          ...AGENT_PROFILES_RETRY_OPTIONS,
        });
        await AgentProfilesService.saveProfile(
          activeProfile.name,
          mergeAgentProfileSaveInput(detail.profile, {
            agent_kind: "acp",
            acp_model: model,
          }),
        );
        return;
      }

      // No active ACP profile: persist as the agent-settings default the
      // legacy launch path reads. The backend deep-merges
      // ``agent_settings_diff`` into the existing ``agent_settings`` dict, so
      // a scalar ``acp_model`` diff updates only the model and preserves the
      // selected provider + command.
      await SettingsService.saveSettings({
        agent_settings_diff: { acp_model: model },
      });
    },
    onSuccess: (_data, { conversationId }) => {
      if (conversationId) {
        invalidateConversationQueries(queryClient, conversationId);
      } else {
        // Mirror useSwitchLlmProfile's home-page path: clear the stale settings
        // cache so the next conversation-start reads the newly saved default,
        // and refetch the settings query so the home-page chip updates. The
        // agent-profiles prefix covers both the list and the detail entry the
        // profile-persist path just rewrote.
        SettingsService.invalidateCache();
        queryClient.invalidateQueries({
          queryKey: SETTINGS_QUERY_KEYS.personal(),
        });
        queryClient.invalidateQueries({
          queryKey: AGENT_PROFILES_QUERY_KEYS.all,
        });
      }
    },
    // No meta.disableToast: unlike useSwitchLlmProfile (which tailors a
    // "Switched to {name} failed" message via its own onError), there's no
    // per-action message worth tailoring here, so the global mutation error
    // toast reports a failed switch / profile or settings write rather than
    // swallowing it.
  });
};