fix(ui): ensure model changes update the UI immediately (#12412)

This commit is contained in:
Abhi
2025-11-03 14:59:51 -05:00
committed by GitHub
parent 59e0b10e6c
commit 265f24e5d7
6 changed files with 94 additions and 3 deletions

View File

@@ -1501,5 +1501,41 @@ describe('AppContainer State Management', () => {
); );
unmount(); unmount();
}); });
it('updates currentModel when ModelChanged event is received', async () => {
// Arrange: Mock initial model
vi.spyOn(mockConfig, 'getModel').mockReturnValue('initial-model');
const { unmount } = render(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);
// Verify initial model
await act(async () => {
await vi.waitFor(() => {
expect(capturedUIState?.currentModel).toBe('initial-model');
});
});
// Get the registered handler for ModelChanged
const handler = mockCoreEvents.on.mock.calls.find(
(call: unknown[]) => call[0] === CoreEvent.ModelChanged,
)?.[1];
expect(handler).toBeDefined();
// Act: Simulate ModelChanged event
act(() => {
handler({ model: 'new-model' });
});
// Assert: Verify model is updated
expect(capturedUIState.currentModel).toBe('new-model');
unmount();
});
}); });
}); });

View File

@@ -48,6 +48,7 @@ import {
debugLogger, debugLogger,
coreEvents, coreEvents,
CoreEvent, CoreEvent,
type ModelChangedPayload,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js'; import { validateAuthMethod } from '../config/auth.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js'; import { loadHierarchicalGeminiMemory } from '../config/config.js';
@@ -258,16 +259,22 @@ export const AppContainer = (props: AppContainerProps) => {
[historyManager.addItem], [historyManager.addItem],
); );
// Subscribe to fallback mode changes from core // Subscribe to fallback mode and model changes from core
useEffect(() => { useEffect(() => {
const handleFallbackModeChanged = () => { const handleFallbackModeChanged = () => {
const effectiveModel = getEffectiveModel(); const effectiveModel = getEffectiveModel();
setCurrentModel(effectiveModel); setCurrentModel(effectiveModel);
}; };
const handleModelChanged = (payload: ModelChangedPayload) => {
setCurrentModel(payload.model);
};
coreEvents.on(CoreEvent.FallbackModeChanged, handleFallbackModeChanged); coreEvents.on(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
return () => { return () => {
coreEvents.off(CoreEvent.FallbackModeChanged, handleFallbackModeChanged); coreEvents.off(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
}; };
}, [getEffectiveModel]); }, [getEffectiveModel]);

View File

@@ -147,6 +147,7 @@ vi.mock('../agents/subagent-tool-wrapper.js', () => ({
const mockCoreEvents = vi.hoisted(() => ({ const mockCoreEvents = vi.hoisted(() => ({
emitFeedback: vi.fn(), emitFeedback: vi.fn(),
emitModelChanged: vi.fn(),
})); }));
const mockSetGlobalProxy = vi.hoisted(() => vi.fn()); const mockSetGlobalProxy = vi.hoisted(() => vi.fn());

View File

@@ -41,6 +41,7 @@ import {
DEFAULT_OTLP_ENDPOINT, DEFAULT_OTLP_ENDPOINT,
uiTelemetryService, uiTelemetryService,
} from '../telemetry/index.js'; } from '../telemetry/index.js';
import { coreEvents } from '../utils/events.js';
import { tokenLimit } from '../core/tokenLimits.js'; import { tokenLimit } from '../core/tokenLimits.js';
import { import {
DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -76,7 +77,6 @@ import type { UserTierId } from '../code_assist/types.js';
import { AgentRegistry } from '../agents/registry.js'; import { AgentRegistry } from '../agents/registry.js';
import { setGlobalProxy } from '../utils/fetch.js'; import { setGlobalProxy } from '../utils/fetch.js';
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js'; import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
import { coreEvents } from '../utils/events.js';
export enum ApprovalMode { export enum ApprovalMode {
DEFAULT = 'default', DEFAULT = 'default',
@@ -711,7 +711,10 @@ export class Config {
return; return;
} }
this.model = newModel; if (this.model !== newModel) {
this.model = newModel;
coreEvents.emitModelChanged(newModel);
}
} }
isInFallbackMode(): boolean { isInFallbackMode(): boolean {

View File

@@ -156,4 +156,17 @@ describe('CoreEventEmitter', () => {
}); });
expect(listener.mock.calls[2][0]).toMatchObject({ message: 'Buffered 2' }); expect(listener.mock.calls[2][0]).toMatchObject({ message: 'Buffered 2' });
}); });
describe('ModelChanged Event', () => {
it('should emit ModelChanged event with correct payload', () => {
const listener = vi.fn();
events.on(CoreEvent.ModelChanged, listener);
const newModel = 'gemini-2.5-pro';
events.emitModelChanged(newModel);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith({ model: newModel });
});
});
}); });

View File

@@ -43,9 +43,20 @@ export interface FallbackModeChangedPayload {
isInFallbackMode: boolean; isInFallbackMode: boolean;
} }
/**
* Payload for the 'model-changed' event.
*/
export interface ModelChangedPayload {
/**
* The new model that was set.
*/
model: string;
}
export enum CoreEvent { export enum CoreEvent {
UserFeedback = 'user-feedback', UserFeedback = 'user-feedback',
FallbackModeChanged = 'fallback-mode-changed', FallbackModeChanged = 'fallback-mode-changed',
ModelChanged = 'model-changed',
} }
export class CoreEventEmitter extends EventEmitter { export class CoreEventEmitter extends EventEmitter {
@@ -86,6 +97,14 @@ export class CoreEventEmitter extends EventEmitter {
this.emit(CoreEvent.FallbackModeChanged, payload); this.emit(CoreEvent.FallbackModeChanged, payload);
} }
/**
* Notifies subscribers that the model has changed.
*/
emitModelChanged(model: string): void {
const payload: ModelChangedPayload = { model };
this.emit(CoreEvent.ModelChanged, payload);
}
/** /**
* Flushes buffered messages. Call this immediately after primary UI listener * Flushes buffered messages. Call this immediately after primary UI listener
* subscribes. * subscribes.
@@ -106,6 +125,10 @@ export class CoreEventEmitter extends EventEmitter {
event: CoreEvent.FallbackModeChanged, event: CoreEvent.FallbackModeChanged,
listener: (payload: FallbackModeChangedPayload) => void, listener: (payload: FallbackModeChangedPayload) => void,
): this; ): this;
override on(
event: CoreEvent.ModelChanged,
listener: (payload: ModelChangedPayload) => void,
): this;
override on( override on(
event: string | symbol, event: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -122,6 +145,10 @@ export class CoreEventEmitter extends EventEmitter {
event: CoreEvent.FallbackModeChanged, event: CoreEvent.FallbackModeChanged,
listener: (payload: FallbackModeChangedPayload) => void, listener: (payload: FallbackModeChangedPayload) => void,
): this; ): this;
override off(
event: CoreEvent.ModelChanged,
listener: (payload: ModelChangedPayload) => void,
): this;
override off( override off(
event: string | symbol, event: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -138,6 +165,10 @@ export class CoreEventEmitter extends EventEmitter {
event: CoreEvent.FallbackModeChanged, event: CoreEvent.FallbackModeChanged,
payload: FallbackModeChangedPayload, payload: FallbackModeChangedPayload,
): boolean; ): boolean;
override emit(
event: CoreEvent.ModelChanged,
payload: ModelChangedPayload,
): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
override emit(event: string | symbol, ...args: any[]): boolean { override emit(event: string | symbol, ...args: any[]): boolean {
return super.emit(event, ...args); return super.emit(event, ...args);