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
10 changes: 5 additions & 5 deletions src/chatbots/ClaudeVoiceMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,10 +767,9 @@ export class ClaudeVoiceMenu extends VoiceSelector {
this.menuContent.appendChild(footnote);
}

// The door to the full catalog, only when the shortlist hides voices.
if (curated.hiddenCount > 0) {
this.menuContent.appendChild(this.createMoreVoicesItem());
}
// The door to the full catalog — always present: it is the navigation
// path to the settings catalog, not an overflow marker (#472).
this.menuContent.appendChild(this.createMoreVoicesItem());

// Add subtle separators for easier scanning
this.applyItemSeparators();
Expand Down Expand Up @@ -815,7 +814,8 @@ export class ClaudeVoiceMenu extends VoiceSelector {

/**
* The muted final row linking to the full voice catalog in the extension
* settings (AI Chat tab). Rendered only when the shortlist hides voices.
* settings (AI Chat tab). Always rendered — it is the menu's path to the
* catalog, whether or not the shortlist is hiding voices.
*/
private createMoreVoicesItem(): HTMLDivElement {
const item = document.createElement("div");
Expand Down
6 changes: 6 additions & 0 deletions src/chatbots/PiVoiceMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export class PiVoiceSettings extends VoiceSelector {
return "saypi-voice-settings";
}

// Pi's own settings grid is uncapped, but still gets the door — it is the
// only path from this surface to the extension's full voice catalog (#472).
protected override showsMoreVoicesDoor(): boolean {
return true;
}

getButtonClasses(): string[] {
return [
"flex",
Expand Down
15 changes: 14 additions & 1 deletion src/tts/VoiceMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ export abstract class VoiceSelector {
return null;
}

/**
* Whether this surface renders the "More voices" door to the settings
* catalog. The door is the navigation path to the full catalog, not an
* overflow marker, so capped surfaces always show it; uncapped surfaces
* opt in (Pi's settings grid does — #472).
*/
protected showsMoreVoicesDoor(): boolean {
return this.getCustomVoiceCap() !== null;
}

async refreshMenu(): Promise<void> {
// remove all voices from the selector
const voiceButtons = Array.from(this.element.querySelectorAll("button"));
Expand Down Expand Up @@ -180,6 +190,9 @@ export abstract class VoiceSelector {
const cap = this.getCustomVoiceCap();
if (cap === null) {
this.populateCustomVoices(customVoices, voiceSelector);
if (this.showsMoreVoicesDoor()) {
this.addMoreVoicesDoor(voiceSelector);
}
return true;
}

Expand All @@ -195,7 +208,7 @@ export abstract class VoiceSelector {
voiceSelector,
curated.tiersCoexist
);
if (curated.hiddenCount > 0) {
if (this.showsMoreVoicesDoor()) {
this.addMoreVoicesDoor(voiceSelector);
}

Expand Down
4 changes: 2 additions & 2 deletions test/chatbots/ClaudeVoiceMenu-curation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ describe("ClaudeVoiceMenu shortlist cap + door (flip-day catalog)", () => {
expect(openSettingsMock).toHaveBeenCalled();
});

it("omits the door row when the whole catalog fits the cap", () => {
it("keeps the door row even when the whole catalog fits the cap (#472)", () => {
const menu = makeMenu();
menu.populateVoices(flipDayCatalog.slice(0, 3), menu.element);
const door = menu.menuContent.querySelector("[data-action='more-voices']");
expect(door).toBeNull();
expect(door).not.toBeNull();
});

it("keeps the Voice off item", () => {
Expand Down
46 changes: 43 additions & 3 deletions test/chatbots/PiVoiceMenu-curation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ vi.mock("../../src/popup/popupopener", () => ({
openSettings: (...args: unknown[]) => openSettingsMock(...args),
}));

import { PiVoiceMenu } from "../../src/chatbots/PiVoiceMenu";
import { PiVoiceMenu, PiVoiceSettings } from "../../src/chatbots/PiVoiceMenu";
import { PI_MENU_CAP } from "../../src/tts/VoiceCuration";
import { ElevenLabsVoice, OpenAIVoice, openAiMockVoices } from "../data/Voices";
import { SpeechSynthesisVoiceRemote } from "../../src/tts/SpeechModel";
Expand Down Expand Up @@ -107,12 +107,52 @@ describe("PiVoiceMenu shortlist cap + door", () => {
expect(openSettingsMock).toHaveBeenCalled();
});

it("omits the door and shows everything when the catalog fits the cap (today's 3 voices)", () => {
it("still shows the door when the catalog fits the cap — it's the path to the catalog, not an overflow marker (#472)", () => {
const menu = makeMenu();
const selector = document.createElement("div");
menu.populateVoices([...piBuiltIns, ...piElevenLabs], selector);
expect(customRows(selector).length).toBe(piElevenLabs.length);
expect(selector.querySelector("button.saypi-more-voices")).toBeNull();
const door = selector.querySelector(
"button.saypi-more-voices"
) as HTMLButtonElement;
expect(door).not.toBeNull();
door.click();
expect(openSettingsMock).toHaveBeenCalled();
});
});

describe("PiVoiceSettings door (#472)", () => {
function makeSettings(): any {
const settings = Object.create(PiVoiceSettings.prototype);
settings.chatbot = {} as any;
settings.userPreferences = {
getVoice: vi.fn(async () => null),
setVoice: vi.fn(async () => {}),
unsetVoice: vi.fn(async () => {}),
};
settings.element = document.createElement("div");
return settings;
}

it("appends a 'More voices' door to the uncapped settings grid", () => {
const settings = makeSettings();
const grid = document.createElement("div");
settings.populateVoices([...piElevenLabs], grid);
expect(customRows(grid).length).toBe(piElevenLabs.length); // grid stays uncapped
const door = grid.querySelector(
"button.saypi-more-voices"
) as HTMLButtonElement;
expect(door).not.toBeNull();
door.click();
expect(openSettingsMock).toHaveBeenCalled();
});

it("does not duplicate the door on repeated populates", () => {
const settings = makeSettings();
const grid = document.createElement("div");
settings.populateVoices([...piElevenLabs], grid);
settings.populateVoices([...piElevenLabs], grid);
expect(grid.querySelectorAll("button.saypi-more-voices").length).toBe(1);
});
});

Expand Down
Loading