fix(goose2): keep model picker and session config in sync (#9048)

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Toohey
2026-05-07 17:30:40 +10:00
committed by GitHub
parent f325a9d8e3
commit f4fe8d5d7f
36 changed files with 1893 additions and 389 deletions
+80 -37
View File
@@ -33,11 +33,8 @@ import { loadStoredHomeSessionId } from "./lib/homeSessionStorage";
import { resolveSupportedSessionModelPreference } from "./lib/resolveSupportedSessionModelPreference";
import { useCreatePersonaNavigation } from "./hooks/useCreatePersonaNavigation";
import { AppShellContent } from "./ui/AppShellContent";
import { acpPrepareSession, acpSetModel } from "@/shared/api/acp";
import {
updateSessionProject,
updateSessionTitle,
} from "@/features/chat/stores/chatSessionOperations";
import { applyLatestSessionConfig } from "@/features/chat/lib/sessionConfigRequests";
import { updateSessionTitle } from "@/features/chat/stores/chatSessionOperations";
import {
clearReplayBuffer,
getAndDeleteReplayBuffer,
@@ -244,14 +241,32 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
}
const request = (async () => {
const currentProvider = () =>
useAgentStore.getState().selectedProvider ?? "goose";
// Resolve the provider to use after an async gap. If the user changed
// their selection while we were awaiting (liveProvider differs from what
// it was before the await), prefer the live value; otherwise use the
// model-preference resolution result.
const resolveProviderAfterAwait = (
providerAtStart: string,
sessionModelPreference: { providerId: string },
): string => {
const liveProvider = currentProvider();
return liveProvider !== providerAtStart
? liveProvider
: sessionModelPreference.providerId;
};
if (
homeSession &&
!homeSession.archivedAt &&
homeSession.messageCount === 0
) {
const providerAtStart = currentProvider();
const sessionModelPreference =
await resolveSupportedSessionModelPreference(
selectedProvider ?? "goose",
providerAtStart,
providerInventoryEntries,
);
const project = homeSession.projectId
@@ -260,41 +275,67 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
) ?? null)
: null;
const workingDir = await resolveSessionCwd(project);
await acpPrepareSession(
homeSession.id,
sessionModelPreference.providerId,
workingDir,
const resolvedProviderId = resolveProviderAfterAwait(
providerAtStart,
sessionModelPreference,
);
const shouldClearHomeModel =
sessionModelPreference.providerId !== homeSession.providerId ||
!sessionModelPreference.modelId;
patchSession(homeSession.id, {
providerId: sessionModelPreference.providerId,
modelId: shouldClearHomeModel ? undefined : homeSession.modelId,
modelName: shouldClearHomeModel ? undefined : homeSession.modelName,
const modelIdToApply =
resolvedProviderId === sessionModelPreference.providerId
? sessionModelPreference.modelId
: undefined;
const result = await applyLatestSessionConfig({
sessionId: homeSession.id,
providerId: resolvedProviderId,
workingDir,
modelId: modelIdToApply,
});
if (sessionModelPreference.modelId) {
await acpSetModel(homeSession.id, sessionModelPreference.modelId);
patchSession(homeSession.id, {
modelId: sessionModelPreference.modelId,
modelName: sessionModelPreference.modelName,
});
if (!result.applied) {
return homeSession;
}
return homeSession;
const shouldClearHomeModel =
resolvedProviderId !== homeSession.providerId || !modelIdToApply;
patchSession(homeSession.id, {
providerId: resolvedProviderId,
modelId:
modelIdToApply ??
(shouldClearHomeModel ? undefined : homeSession.modelId),
modelName:
modelIdToApply != null
? sessionModelPreference.modelName
: shouldClearHomeModel
? undefined
: homeSession.modelName,
});
return (
useChatSessionStore.getState().getSession(homeSession.id) ??
homeSession
);
}
const providerAtStart = currentProvider();
const workingDir = await resolveSessionCwd(null);
const sessionModelPreference =
await resolveSupportedSessionModelPreference(
selectedProvider ?? "goose",
providerAtStart,
providerInventoryEntries,
);
const resolvedProviderId = resolveProviderAfterAwait(
providerAtStart,
sessionModelPreference,
);
const session = await createSession({
title: DEFAULT_CHAT_TITLE,
providerId: sessionModelPreference.providerId,
providerId: resolvedProviderId,
workingDir,
modelId: sessionModelPreference.modelId,
modelName: sessionModelPreference.modelName,
modelId:
resolvedProviderId === sessionModelPreference.providerId
? sessionModelPreference.modelId
: undefined,
modelName:
resolvedProviderId === sessionModelPreference.providerId
? sessionModelPreference.modelName
: undefined,
});
setHomeSessionId(session.id);
return session;
@@ -309,7 +350,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
}
}
}, [
selectedProvider,
createSession,
hasHydratedSessions,
homeSession,
@@ -512,14 +552,14 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
const handleMoveToProject = useCallback(
(sessionId: string, projectId: string | null) => {
useChatSessionStore.getState().patchSession(sessionId, { projectId });
const session = useChatSessionStore.getState().getSession(sessionId);
if (!session) {
return;
}
void (async () => {
await updateSessionProject(sessionId, projectId);
const nextProject =
projectId == null
? null
@@ -530,17 +570,20 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
if (!workingDir) {
return;
}
await acpPrepareSession(
await applyLatestSessionConfig({
sessionId,
session.providerId ?? selectedProvider ?? "goose",
providerId: session.providerId ?? selectedProvider ?? "goose",
workingDir,
);
modelId: session.modelId,
});
})().catch((error) => {
console.error("Failed to move chat to project:", error);
toast.error(t("notifications.moveError"));
console.error(
"Failed to update ACP session project working directory:",
error,
);
});
},
[selectedProvider, t],
[selectedProvider],
);
const handleRenameChat = useCallback(
+4 -2
View File
@@ -65,6 +65,7 @@ export function useAppStartup() {
const applyProvidersFromInventory = (
entries: Parameters<typeof discoverAcpProvidersFromEntries>[0],
validated = false,
) => {
const providers = discoverAcpProvidersFromEntries(entries);
const providerAllowlist = parseProviderAllowlist(
@@ -76,6 +77,7 @@ export function useAppStartup() {
providerAllowlist,
getModelProviders(),
),
validated,
);
return providers;
};
@@ -116,7 +118,7 @@ export function useAppStartup() {
...useProviderInventoryStore.getState().entries.values(),
];
if (inventoryEntries.length > 0) {
applyProvidersFromInventory(inventoryEntries);
applyProvidersFromInventory(inventoryEntries, true);
}
perfLog(
`[perf:startup] loadProviderCatalog done in ${(performance.now() - t0).toFixed(1)}ms (n=${entries.length})`,
@@ -140,7 +142,7 @@ export function useAppStartup() {
inventoryStore.setEntries(entries);
// Derive ACP providers from the same response
const providers = applyProvidersFromInventory(entries);
const providers = applyProvidersFromInventory(entries, true);
perfLog(
`[perf:startup] loadProvidersAndInventory done in ${(performance.now() - t0).toFixed(1)}ms (entries=${entries.length}, providers=${providers.length})`,
@@ -55,4 +55,28 @@ describe("resolveSupportedSessionModelPreference", () => {
providerId: "openai",
});
});
it("preserves an exact stored provider model while inventory is unavailable", async () => {
window.localStorage.setItem(
"goose:preferredModelsByAgent",
JSON.stringify({
"claude-acp": {
modelId: "opus",
modelName: "Claude Opus",
providerId: "claude-acp",
},
}),
);
mockGetProviderInventory.mockRejectedValue(
new Error("inventory unavailable"),
);
await expect(
resolveSupportedSessionModelPreference("claude-acp", new Map()),
).resolves.toEqual({
providerId: "claude-acp",
modelId: "opus",
modelName: "Claude Opus",
});
});
});
@@ -5,6 +5,7 @@ import {
sanitizeSessionModelPreference,
type SessionModelPreference,
} from "@/features/chat/lib/sessionModelPreference";
import { getStoredModelPreference } from "@/features/chat/lib/modelPreferences";
export async function resolveSupportedSessionModelPreference(
providerId: string,
@@ -20,6 +21,15 @@ export async function resolveSupportedSessionModelPreference(
return sessionModelPreference;
}
const exactStoredPreference = preferredModel
? null
: getStoredModelPreference(providerId);
const shouldPreserveWithoutInventory =
sessionModelPreference.providerId === providerId &&
exactStoredPreference?.modelId === sessionModelPreference.modelId &&
(exactStoredPreference.providerId ?? providerId) ===
sessionModelPreference.providerId;
const inventoryEntry =
inventoryEntries.get(sessionModelPreference.providerId) ??
(await getProviderInventory([sessionModelPreference.providerId])
@@ -27,6 +37,10 @@ export async function resolveSupportedSessionModelPreference(
.catch(() => undefined));
if (!inventoryEntry) {
if (shouldPreserveWithoutInventory) {
return sessionModelPreference;
}
return {
providerId: sessionModelPreference.providerId,
};