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
76 changes: 45 additions & 31 deletions src/chatpanel/features/composer/input-toolbar.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { GEMINI_SETTINGS_STORAGE_KEY, normalizeGeminiSettings } from '../../../shared/settings';
import {
DEFAULT_GEMINI_MODEL,
GEMINI_SETTINGS_STORAGE_KEY,
getModelDisplayLabel,
getModelThinkingLevels,
normalizeGeminiSettings,
} from '../../../shared/settings';
import { queryRequiredElement } from '../../core/dom';
import { createMenuController } from '../../core/menu-controller';

Expand All @@ -10,23 +16,6 @@ export interface InputToolbar {
attachButton: HTMLButtonElement;
}

const MODEL_ALIASES: Record<string, string> = {
'gemini-3-flash-preview': 'Flash',
'gemini-3.1-pro-preview': 'Pro',
};

const BUILTIN_THINKING_LEVELS: Record<string, string[]> = {
'gemini-3-flash-preview': ['minimal', 'low', 'medium', 'high'],
'gemini-3.1-pro-preview': ['low', 'medium', 'high'],
};

const DEFAULT_THINKING_DEFAULTS: Record<string, string> = {
'gemini-3-flash-preview': 'minimal',
'gemini-3.1-pro-preview': 'high',
};

const DEFAULT_THINKING_LEVELS = ['low', 'medium', 'high'];

const THINKING_LABELS: Record<string, string> = {
high: 'High',
medium: 'Med',
Expand Down Expand Up @@ -58,12 +47,17 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar {
const attachButton = queryRequiredElement<HTMLButtonElement>(shadowRoot, '#speakeasy-attach');
const modelMenuController = createMenuController({ container: modelDropup });
const thinkingMenuController = createMenuController({ container: thinkingDropup });
let modelThinkingLevelMap = normalizeGeminiSettings(undefined).modelThinkingLevelMap;

function closeAllDropups(): void {
modelMenuController.setOpen(false);
thinkingMenuController.setOpen(false);
}

function selectedModelValue(): string {
return modelTrigger.dataset.value ?? DEFAULT_GEMINI_MODEL;
}

function selectDropupItem(
dropup: HTMLElement,
trigger: HTMLButtonElement,
Expand All @@ -83,24 +77,36 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar {
}
}

function updateThinkingOptions(model: string): void {
const levels = BUILTIN_THINKING_LEVELS[model] ?? DEFAULT_THINKING_LEVELS;
const defaultLevel = DEFAULT_THINKING_DEFAULTS[model] ?? 'high';
function resolveThinkingLevel(model: string, preferred?: string): string {
const levels: readonly string[] = getModelThinkingLevels(model);
if (preferred && levels.includes(preferred)) {
return preferred;
}
const mapped = modelThinkingLevelMap[model];
if (mapped && levels.includes(mapped)) {
return mapped;
}
return levels[levels.length - 1] ?? 'high';
}

function updateThinkingOptions(model: string, preferredThinkingLevel?: string): void {
const levels = getModelThinkingLevels(model);
const selectedThinkingLevel = resolveThinkingLevel(model, preferredThinkingLevel);
thinkingMenu.innerHTML = '';
for (const level of [...levels].reverse()) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'dropup-item';
btn.dataset.value = level;
btn.textContent = THINKING_LABELS[level] ?? level;
btn.setAttribute('aria-selected', level === defaultLevel ? 'true' : 'false');
btn.setAttribute('aria-selected', level === selectedThinkingLevel ? 'true' : 'false');
thinkingMenu.appendChild(btn);
}
selectDropupItem(
thinkingDropup,
thinkingTrigger,
defaultLevel,
THINKING_LABELS[defaultLevel] ?? defaultLevel,
selectedThinkingLevel,
THINKING_LABELS[selectedThinkingLevel] ?? selectedThinkingLevel,
);
}

Expand All @@ -120,7 +126,7 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar {
btn.className = 'dropup-item';
btn.dataset.value = model;
btn.dataset.custom = '1';
btn.textContent = MODEL_ALIASES[model] ?? model;
btn.textContent = getModelDisplayLabel(model);
modelMenu.appendChild(btn);
}
}
Expand Down Expand Up @@ -171,26 +177,34 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar {
closeAllDropups();
});

void chrome.storage.local.get(GEMINI_SETTINGS_STORAGE_KEY).then((stored) => {
const settings = normalizeGeminiSettings(stored[GEMINI_SETTINGS_STORAGE_KEY]);
function applySettings(settingsValue: unknown, keepThinkingSelection = false): void {
const settings = normalizeGeminiSettings(settingsValue);
modelThinkingLevelMap = settings.modelThinkingLevelMap;
applyCustomModels(settings.customModels);
const preferred = keepThinkingSelection ? thinkingTrigger.dataset.value : undefined;
updateThinkingOptions(selectedModelValue(), preferred);
}

applySettings(undefined);

void chrome.storage.local.get(GEMINI_SETTINGS_STORAGE_KEY).then((stored) => {
applySettings(stored[GEMINI_SETTINGS_STORAGE_KEY]);
});

chrome.storage.onChanged.addListener((changes) => {
const settingsChange = changes[GEMINI_SETTINGS_STORAGE_KEY];
if (!settingsChange) {
return;
}
const settings = normalizeGeminiSettings(settingsChange.newValue);
applyCustomModels(settings.customModels);
applySettings(settingsChange.newValue, true);
});

return {
selectedModel(): string {
return modelTrigger.dataset.value ?? 'gemini-3-flash-preview';
return selectedModelValue();
},
selectedThinkingLevel(): string {
return thinkingTrigger.dataset.value ?? 'minimal';
return resolveThinkingLevel(selectedModelValue(), thinkingTrigger.dataset.value);
},
captureButton,
extractTextButton,
Expand Down
51 changes: 39 additions & 12 deletions src/chatpanel/template/composer-template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,39 @@
import {
BUILTIN_GEMINI_MODEL_CATALOG,
type ThinkingLevel,
getBuiltinGeminiModelByKey,
} from '../../shared/settings';

const THINKING_LABELS: Record<ThinkingLevel, string> = {
high: 'High',
medium: 'Med',
low: 'Low',
minimal: 'Min',
};

function renderModelMenuItems(selectedModel: string): string {
return BUILTIN_GEMINI_MODEL_CATALOG.map((entry) => {
const selected = entry.model === selectedModel ? ' aria-selected="true"' : '';
return `<button type="button" class="dropup-item" data-value="${entry.model}"${selected}>${entry.label}</button>`;
}).join('');
}

function renderThinkingMenuItems(
levels: readonly ThinkingLevel[],
selectedLevel: ThinkingLevel,
): string {
return [...levels]
.reverse()
.map((level) => {
const selected = level === selectedLevel ? ' aria-selected="true"' : '';
return `<button type="button" class="dropup-item" data-value="${level}"${selected}>${THINKING_LABELS[level] ?? level}</button>`;
})
.join('');
}

export function getComposerTemplate(): string {
const defaultModel = getBuiltinGeminiModelByKey('flash');
const defaultThinkingLevel = defaultModel.defaultThinkingLevel;
return `
<form id="speakeasy-form" class="composer" autocomplete="off">
<div class="composer-inner">
Expand All @@ -20,21 +55,13 @@ export function getComposerTemplate(): string {
</div>
<div class="input-toolbar">
<div class="dropup" id="speakeasy-model-dropup">
<button type="button" class="dropup-trigger" data-value="gemini-3-flash-preview" title="Select model">Flash</button>
<div class="dropup-menu">
<button type="button" class="dropup-item" data-value="gemini-3-flash-preview" aria-selected="true">Flash</button>
<button type="button" class="dropup-item" data-value="gemini-3.1-pro-preview">Pro</button>
</div>
<button type="button" class="dropup-trigger" data-value="${defaultModel.model}" title="Select model">${defaultModel.label}</button>
<div class="dropup-menu">${renderModelMenuItems(defaultModel.model)}</div>
</div>
<span class="input-toolbar-separator" aria-hidden="true">|</span>
<div class="dropup" id="speakeasy-thinking-dropup">
<button type="button" class="dropup-trigger" data-value="minimal" title="Select effort level">Min</button>
<div class="dropup-menu">
<button type="button" class="dropup-item" data-value="high">High</button>
<button type="button" class="dropup-item" data-value="medium">Med</button>
<button type="button" class="dropup-item" data-value="low">Low</button>
<button type="button" class="dropup-item" data-value="minimal" aria-selected="true">Min</button>
</div>
<button type="button" class="dropup-trigger" data-value="${defaultThinkingLevel}" title="Select effort level">${THINKING_LABELS[defaultThinkingLevel] ?? defaultThinkingLevel}</button>
<div class="dropup-menu">${renderThinkingMenuItems(defaultModel.thinkingLevels, defaultThinkingLevel)}</div>
</div>
<div class="input-toolbar-actions">
<button
Expand Down
20 changes: 18 additions & 2 deletions src/options/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ export interface OptionsDom {
versionNode: HTMLElement;
statusNode: HTMLElement;
apiKeyInput: HTMLInputElement;
modelInput: HTMLInputElement;
modelFlashNameInput: HTMLInputElement;
modelFlashThinkingLevelSelect: HTMLSelectElement;
modelProNameInput: HTMLInputElement;
modelProThinkingLevelSelect: HTMLSelectElement;
customModelRowsContainer: HTMLElement;
addCustomModelButton: HTMLButtonElement;
customModelRowTemplate: HTMLTemplateElement;
systemInstructionInput: HTMLTextAreaElement;
storeInteractionsInput: HTMLInputElement;
maxToolRoundTripsInput: HTMLInputElement;
Expand All @@ -29,7 +35,17 @@ export function getOptionsDom(): OptionsDom {
versionNode: queryRequiredElement<HTMLElement>('#version'),
statusNode: queryRequiredElement<HTMLElement>('#save-status'),
apiKeyInput: queryRequiredElement<HTMLInputElement>('#api-key'),
modelInput: queryRequiredElement<HTMLInputElement>('#model'),
modelFlashNameInput: queryRequiredElement<HTMLInputElement>('#model-name-flash'),
modelFlashThinkingLevelSelect: queryRequiredElement<HTMLSelectElement>(
'#model-thinking-level-flash',
),
modelProNameInput: queryRequiredElement<HTMLInputElement>('#model-name-pro'),
modelProThinkingLevelSelect: queryRequiredElement<HTMLSelectElement>(
'#model-thinking-level-pro',
),
customModelRowsContainer: queryRequiredElement<HTMLElement>('#custom-model-rows'),
addCustomModelButton: queryRequiredElement<HTMLButtonElement>('#add-custom-model'),
customModelRowTemplate: queryRequiredElement<HTMLTemplateElement>('#custom-model-row-template'),
systemInstructionInput: queryRequiredElement<HTMLTextAreaElement>('#system-instruction'),
storeInteractionsInput: queryRequiredElement<HTMLInputElement>('#store-interactions'),
maxToolRoundTripsInput: queryRequiredElement<HTMLInputElement>('#max-tool-round-trips'),
Expand Down
Loading