Skip to content

Commit 5bd73a3

Browse files
fix(patch): cherry-pick 265f24e to release/v0.12.0-preview.9-pr-12412 [CONFLICTS] (#12498)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com> Co-authored-by: Abhi <abhipatel@google.com>
1 parent 1c44586 commit 5bd73a3

6 files changed

Lines changed: 109 additions & 3 deletions

File tree

packages/cli/src/ui/AppContainer.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import {
4747
UIActionsContext,
4848
type UIActions,
4949
} from './contexts/UIActionsContext.js';
50-
import { useContext } from 'react';
50+
import { useContext, act } from 'react';
5151

5252
// Mock useStdout to capture terminal title writes
5353
let mockStdout: { write: ReturnType<typeof vi.fn> };
@@ -1395,5 +1395,41 @@ describe('AppContainer State Management', () => {
13951395
expect.any(Number),
13961396
);
13971397
});
1398+
1399+
it('updates currentModel when ModelChanged event is received', async () => {
1400+
// Arrange: Mock initial model
1401+
vi.spyOn(mockConfig, 'getModel').mockReturnValue('initial-model');
1402+
1403+
const { unmount } = render(
1404+
<AppContainer
1405+
config={mockConfig}
1406+
settings={mockSettings}
1407+
version="1.0.0"
1408+
initializationResult={mockInitResult}
1409+
/>,
1410+
);
1411+
1412+
// Verify initial model
1413+
await act(async () => {
1414+
await vi.waitFor(() => {
1415+
expect(capturedUIState?.currentModel).toBe('initial-model');
1416+
});
1417+
});
1418+
1419+
// Get the registered handler for ModelChanged
1420+
const handler = mockCoreEvents.on.mock.calls.find(
1421+
(call: unknown[]) => call[0] === CoreEvent.ModelChanged,
1422+
)?.[1];
1423+
expect(handler).toBeDefined();
1424+
1425+
// Act: Simulate ModelChanged event
1426+
act(() => {
1427+
handler({ model: 'new-model' });
1428+
});
1429+
1430+
// Assert: Verify model is updated
1431+
expect(capturedUIState.currentModel).toBe('new-model');
1432+
unmount();
1433+
});
13981434
});
13991435
});

packages/cli/src/ui/AppContainer.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
debugLogger,
4848
coreEvents,
4949
CoreEvent,
50+
type ModelChangedPayload,
5051
} from '@google/gemini-cli-core';
5152
import { validateAuthMethod } from '../config/auth.js';
5253
import { loadHierarchicalGeminiMemory } from '../config/config.js';
@@ -253,16 +254,22 @@ export const AppContainer = (props: AppContainerProps) => {
253254
[historyManager.addItem],
254255
);
255256

256-
// Subscribe to fallback mode changes from core
257+
// Subscribe to fallback mode and model changes from core
257258
useEffect(() => {
258259
const handleFallbackModeChanged = () => {
259260
const effectiveModel = getEffectiveModel();
260261
setCurrentModel(effectiveModel);
261262
};
262263

264+
const handleModelChanged = (payload: ModelChangedPayload) => {
265+
setCurrentModel(payload.model);
266+
};
267+
263268
coreEvents.on(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
269+
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
264270
return () => {
265271
coreEvents.off(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
272+
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
266273
};
267274
}, [getEffectiveModel]);
268275

packages/core/src/config/config.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ vi.mock('../agents/subagent-tool-wrapper.js', () => ({
139139
SubagentToolWrapper: vi.fn(),
140140
}));
141141

142+
const mockCoreEvents = vi.hoisted(() => ({
143+
emitFeedback: vi.fn(),
144+
emitModelChanged: vi.fn(),
145+
}));
146+
147+
const mockSetGlobalProxy = vi.hoisted(() => vi.fn());
148+
149+
vi.mock('../utils/events.js', () => ({
150+
coreEvents: mockCoreEvents,
151+
}));
152+
153+
vi.mock('../utils/fetch.js', () => ({
154+
setGlobalProxy: mockSetGlobalProxy,
155+
}));
156+
142157
import { BaseLlmClient } from '../core/baseLlmClient.js';
143158
import { tokenLimit } from '../core/tokenLimits.js';
144159
import { uiTelemetryService } from '../telemetry/index.js';

packages/core/src/config/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
DEFAULT_OTLP_ENDPOINT,
4242
uiTelemetryService,
4343
} from '../telemetry/index.js';
44+
import { coreEvents } from '../utils/events.js';
4445
import { tokenLimit } from '../core/tokenLimits.js';
4546
import {
4647
DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -638,7 +639,10 @@ export class Config {
638639
return;
639640
}
640641

641-
this.model = newModel;
642+
if (this.model !== newModel) {
643+
this.model = newModel;
644+
coreEvents.emitModelChanged(newModel);
645+
}
642646
}
643647

644648
isInFallbackMode(): boolean {

packages/core/src/utils/events.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,17 @@ describe('CoreEventEmitter', () => {
156156
});
157157
expect(listener.mock.calls[2][0]).toMatchObject({ message: 'Buffered 2' });
158158
});
159+
160+
describe('ModelChanged Event', () => {
161+
it('should emit ModelChanged event with correct payload', () => {
162+
const listener = vi.fn();
163+
events.on(CoreEvent.ModelChanged, listener);
164+
165+
const newModel = 'gemini-2.5-pro';
166+
events.emitModelChanged(newModel);
167+
168+
expect(listener).toHaveBeenCalledTimes(1);
169+
expect(listener).toHaveBeenCalledWith({ model: newModel });
170+
});
171+
});
159172
});

packages/core/src/utils/events.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,20 @@ export interface FallbackModeChangedPayload {
4343
isInFallbackMode: boolean;
4444
}
4545

46+
/**
47+
* Payload for the 'model-changed' event.
48+
*/
49+
export interface ModelChangedPayload {
50+
/**
51+
* The new model that was set.
52+
*/
53+
model: string;
54+
}
55+
4656
export enum CoreEvent {
4757
UserFeedback = 'user-feedback',
4858
FallbackModeChanged = 'fallback-mode-changed',
59+
ModelChanged = 'model-changed',
4960
}
5061

5162
export class CoreEventEmitter extends EventEmitter {
@@ -86,6 +97,14 @@ export class CoreEventEmitter extends EventEmitter {
8697
this.emit(CoreEvent.FallbackModeChanged, payload);
8798
}
8899

100+
/**
101+
* Notifies subscribers that the model has changed.
102+
*/
103+
emitModelChanged(model: string): void {
104+
const payload: ModelChangedPayload = { model };
105+
this.emit(CoreEvent.ModelChanged, payload);
106+
}
107+
89108
/**
90109
* Flushes buffered messages. Call this immediately after primary UI listener
91110
* subscribes.
@@ -106,6 +125,10 @@ export class CoreEventEmitter extends EventEmitter {
106125
event: CoreEvent.FallbackModeChanged,
107126
listener: (payload: FallbackModeChangedPayload) => void,
108127
): this;
128+
override on(
129+
event: CoreEvent.ModelChanged,
130+
listener: (payload: ModelChangedPayload) => void,
131+
): this;
109132
override on(
110133
event: string | symbol,
111134
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -122,6 +145,10 @@ export class CoreEventEmitter extends EventEmitter {
122145
event: CoreEvent.FallbackModeChanged,
123146
listener: (payload: FallbackModeChangedPayload) => void,
124147
): this;
148+
override off(
149+
event: CoreEvent.ModelChanged,
150+
listener: (payload: ModelChangedPayload) => void,
151+
): this;
125152
override off(
126153
event: string | symbol,
127154
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -138,6 +165,10 @@ export class CoreEventEmitter extends EventEmitter {
138165
event: CoreEvent.FallbackModeChanged,
139166
payload: FallbackModeChangedPayload,
140167
): boolean;
168+
override emit(
169+
event: CoreEvent.ModelChanged,
170+
payload: ModelChangedPayload,
171+
): boolean;
141172
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142173
override emit(event: string | symbol, ...args: any[]): boolean {
143174
return super.emit(event, ...args);

0 commit comments

Comments
 (0)