diff --git a/src/agentMode/session/AgentSession.test.ts b/src/agentMode/session/AgentSession.test.ts index 26d043ef..584347a9 100644 --- a/src/agentMode/session/AgentSession.test.ts +++ b/src/agentMode/session/AgentSession.test.ts @@ -848,6 +848,47 @@ describe("AgentSession intent capabilities", () => { }).canSwitchMode() ).toBe(true); }); + + it("canSwitch* return false while the session status is starting", async () => { + const mock = makeMockBackend(); + // Keep newSession pending so status stays "starting". + let resolveNew: ((value: { sessionId: string; state: BackendState }) => void) | null = null; + mock.newSession.mockReturnValueOnce( + new Promise((resolve) => { + resolveNew = resolve; + }) + ); + mock.asBackend.isSetSessionModelSupported = () => true; + mock.asBackend.isSetSessionConfigOptionSupported = () => true; + mock.asBackend.isSetSessionModeSupported = () => true; + const session = AgentSession.start({ + backend: mock.asBackend, + cwd: "/vault", + internalId: "internal-1", + backendId: "test-backend", + getDescriptor: () => undefined, + }); + expect(session.getStatus()).toBe("starting"); + expect(session.canSwitchModel()).toBe(false); + expect(session.canSwitchEffort()).toBe(false); + expect(session.canSwitchMode()).toBe(false); + + // After ready, the gate lifts and the underlying probes drive the answer. + resolveNew!({ + sessionId: "acp-1", + state: { + model: null, + mode: { + current: "plan", + options: [{ value: "plan", label: "Plan" }], + apply: { plan: { kind: "setMode", nativeId: "plan" } }, + }, + }, + }); + await session.ready; + expect(session.canSwitchModel()).toBe(true); + expect(session.canSwitchMode()).toBe(true); + }); }); describe("AgentSession.setLabel", () => { diff --git a/src/agentMode/session/AgentSession.ts b/src/agentMode/session/AgentSession.ts index 928b8fdb..569cee4b 100644 --- a/src/agentMode/session/AgentSession.ts +++ b/src/agentMode/session/AgentSession.ts @@ -333,6 +333,7 @@ export class AgentSession { /** Whether the user can swap the active model on this session. */ canSwitchModel(): boolean | null { + if (this.status === "starting") return false; return this.backend.isSetSessionModelSupported(); } @@ -342,6 +343,7 @@ export class AgentSession { * routing is encapsulated here — UI consumers ask intent only. */ canSwitchEffort(): boolean | null { + if (this.status === "starting") return false; const descriptor = this.getDescriptor?.(); if (!descriptor) return null; return descriptor.wire.effortConfigFor @@ -355,6 +357,7 @@ export class AgentSession { * the dispatch path is consistent, so we sample the first option. */ canSwitchMode(): boolean | null { + if (this.status === "starting") return false; const mode = this.currentState?.mode; if (!mode) return null; const sample = mode.options[0]; diff --git a/src/agentMode/ui/useAgentModePicker.ts b/src/agentMode/ui/useAgentModePicker.ts index f2254b73..91c9b1f3 100644 --- a/src/agentMode/ui/useAgentModePicker.ts +++ b/src/agentMode/ui/useAgentModePicker.ts @@ -29,9 +29,14 @@ function useAgentModeSignal(manager: AgentSessionManager | null): string { if (!manager) return ""; const session = manager.getActiveSession(); const state = session?.getState() ?? manager.getCachedBackendState(session?.backendId ?? ""); - return [session?.internalId ?? "", session?.backendId ?? "", modeStateSignature(state)].join( - "|" - ); + return [ + session?.internalId ?? "", + session?.backendId ?? "", + // Include status so the picker's `disabled` flips when the session + // transitions out of "starting" (canSwitchMode gates on status). + session?.getStatus() ?? "", + modeStateSignature(state), + ].join("|"); }, [manager]); return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); diff --git a/src/agentMode/ui/useAgentModelPicker.ts b/src/agentMode/ui/useAgentModelPicker.ts index 35f7818b..c2d37b78 100644 --- a/src/agentMode/ui/useAgentModelPicker.ts +++ b/src/agentMode/ui/useAgentModelPicker.ts @@ -61,6 +61,9 @@ function useAgentModelSignal( const parts: string[] = [ session?.internalId ?? "", session?.backendId ?? "", + // Include status so the picker's `disabled` flips when the session + // transitions out of "starting" (canSwitchModel/Effort gate on status). + session?.getStatus() ?? "", session?.hasUserVisibleMessages() ? "1" : "0", ]; for (const d of descriptors) {