mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 14:40:52 -07:00
fix(ui): ensure model changes update the UI immediately (#12412)
This commit is contained in:
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user