Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/pages/content/defaultModel/__tests__/modelLocker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,90 @@ describe('DefaultModelManager (default model locker)', () => {
expect(selectorBtn.click).toHaveBeenCalledTimes(0);
});

it('does not inject star buttons into the settings menu (desktop-settings-menu)', async () => {
const { default: DefaultModelManager } = await import('../modelLocker');
await DefaultModelManager.getInstance().init();
destroyManager = () => DefaultModelManager.getInstance().destroy();

// Simulate the Gemini settings/profile dropdown (has class desktop-settings-menu)
const settingsMenu = document.createElement('div');
settingsMenu.className = 'mat-mdc-menu-panel collapsed desktop-settings-menu ia-redesign';
settingsMenu.setAttribute('role', 'menu');

const settingsItem = document.createElement('a');
settingsItem.setAttribute('role', 'menuitem');
settingsItem.innerHTML = `
<span class="mat-mdc-menu-item-text">
<div class="menu-entry-with-badge">
<span class="gds-label-l">个人使用场景</span>
</div>
</span>
`;
settingsMenu.appendChild(settingsItem);

const themeItem = document.createElement('button');
themeItem.setAttribute('role', 'menuitem');
themeItem.innerHTML = `
<span class="mat-mdc-menu-item-text">
<span class="gds-label-l">主题</span>
</span>
`;
settingsMenu.appendChild(themeItem);

document.body.appendChild(settingsMenu);

await Promise.resolve();
await vi.advanceTimersByTimeAsync(500);

// Star buttons should NOT be injected into settings menu items
expect(settingsItem.querySelector('.gv-default-star-btn')).toBeNull();
expect(themeItem.querySelector('.gv-default-star-btn')).toBeNull();
});

it('does not inject star buttons into the theme submenu (menuitemradio without model markers)', async () => {
const { default: DefaultModelManager } = await import('../modelLocker');
await DefaultModelManager.getInstance().init();
destroyManager = () => DefaultModelManager.getInstance().destroy();

// Simulate the Gemini theme picker submenu (has menuitemradio but no model markers)
const themeMenu = document.createElement('div');
themeMenu.className = 'mat-mdc-menu-panel';
themeMenu.setAttribute('role', 'menu');

const systemItem = document.createElement('button');
systemItem.setAttribute('role', 'menuitemradio');
systemItem.setAttribute('aria-checked', 'false');
systemItem.innerHTML = `
<span class="mat-mdc-menu-item-text">
<span class="menu-item-title-with-trailing-component">
<span class="gds-label-l">系统</span>
</span>
</span>
`;
themeMenu.appendChild(systemItem);

const darkItem = document.createElement('button');
darkItem.setAttribute('role', 'menuitemradio');
darkItem.setAttribute('aria-checked', 'true');
darkItem.innerHTML = `
<span class="mat-mdc-menu-item-text">
<span class="menu-item-title-with-trailing-component">
<span class="gds-label-l">深色</span>
</span>
</span>
`;
themeMenu.appendChild(darkItem);

document.body.appendChild(themeMenu);

await Promise.resolve();
await vi.advanceTimersByTimeAsync(500);

// Star buttons should NOT be injected into theme menu items
expect(systemItem.querySelector('.gv-default-star-btn')).toBeNull();
expect(darkItem.querySelector('.gv-default-star-btn')).toBeNull();
});

it('stops retrying after consecutive failures when target model is not found', async () => {
// Set default model to a model that won't be found
(chrome.storage.sync.get as unknown as ReturnType<typeof vi.fn>).mockImplementation(
Expand Down
19 changes: 16 additions & 3 deletions src/pages/content/defaultModel/modelLocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const FAST_MODEL_NAMES = ['flash', '2.0 flash', 'gemini 2.0 flash', 'fast', '高
// Gemini may use either role="menuitemradio" or role="menuitem" depending on the UI variant.
const MODE_ITEM_SELECTOR = '[role="menuitemradio"], [role="menuitem"]';

// Fallback selector that excludes known non-model menus (e.g. the settings/profile dropdown).
const NON_MODEL_MENU_EXCLUSION_FALLBACK =
'.mat-mdc-menu-panel[role="menu"]:not(.desktop-settings-menu)';

const CHAT_INPUT_SELECTORS = [
'main rich-textarea [contenteditable="true"]',
'rich-textarea [contenteditable="true"]',
Expand Down Expand Up @@ -184,15 +188,15 @@ class DefaultModelManager {
if (
root.matches('.mat-mdc-menu-panel.gds-mode-switch-menu[role="menu"]') ||
root.matches('mat-action-list.gds-mode-switch-menu-list') ||
root.matches('.mat-mdc-menu-panel[role="menu"]')
root.matches(NON_MODEL_MENU_EXCLUSION_FALLBACK)
) {
return root;
}

return (
root.querySelector<HTMLElement>('.mat-mdc-menu-panel.gds-mode-switch-menu[role="menu"]') ??
root.querySelector<HTMLElement>('mat-action-list.gds-mode-switch-menu-list') ??
root.querySelector<HTMLElement>('.mat-mdc-menu-panel[role="menu"]')
root.querySelector<HTMLElement>(NON_MODEL_MENU_EXCLUSION_FALLBACK)
);
}

Expand All @@ -202,7 +206,7 @@ class DefaultModelManager {
'.mat-mdc-menu-panel.gds-mode-switch-menu[role="menu"]',
) ??
document.querySelector<HTMLElement>('mat-action-list.gds-mode-switch-menu-list') ??
document.querySelector<HTMLElement>('.mat-mdc-menu-panel[role="menu"]')
document.querySelector<HTMLElement>(NON_MODEL_MENU_EXCLUSION_FALLBACK)
);
}

Expand Down Expand Up @@ -250,6 +254,15 @@ class DefaultModelManager {
const items = menuPanel.querySelectorAll(MODE_ITEM_SELECTOR);
if (!items.length) return false;

// Guard: only inject into menus that look like a model selector.
// Model menus always contain .title-and-description, .mode-title, or data-mode-id.
// Non-model menus (theme picker, help, etc.) lack these even if they use menuitemradio.
const isModelMenu =
menuPanel.querySelector('[data-mode-id]') !== null ||
menuPanel.querySelector('.mode-title') !== null ||
menuPanel.querySelector('.title-and-description') !== null;
if (!isModelMenu) return false;

// Use cached value efficiently
if (!this.initialized) {
const result = await storageService.get<unknown>(StorageKeys.DEFAULT_MODEL);
Expand Down
Loading