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
37 changes: 15 additions & 22 deletions src/background/features/gemini/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import type { FileDataAttachmentPayload } from '../../../shared/runtime';
import type { GeminiSettings } from '../../../shared/settings';
import { isRecord, toErrorMessage } from '../../core/utils';
import type { ChatSession, GeminiContent } from '../session/types';
import { getGeminiClient } from './gemini-client';
import { composeGeminiInteractionRequest } from './gemini-request';
import { normalizeContent } from './gemini/content-normalize';
import {
extractAttachments,
renderContentForChat,
renderThinkingSummaryForChat,
} from './gemini/content-render';
import type { GeminiStreamDelta, SDKCreateInteractionRequest } from './gemini/contracts';
import type { GeminiStreamDelta } from './gemini/contracts';
import {
InvalidPreviousInteractionIdError,
isInvalidPreviousInteractionIdError,
Expand Down Expand Up @@ -202,7 +201,7 @@ async function callGeminiInteractionWithFunctionResultRetry(input: {
}): Promise<Awaited<ReturnType<typeof callGeminiInteraction>>> {
try {
return await callGeminiInteractionWithOptionalStreaming({
settings: input.settings,
apiKey: input.settings.apiKey,
request: input.requestPlan.request,
...(input.onStreamDelta ? { onStreamDelta: input.onStreamDelta } : {}),
});
Expand All @@ -229,28 +228,28 @@ async function callGeminiInteractionWithFunctionResultRetry(input: {
});

return callGeminiInteractionWithOptionalStreaming({
settings: input.settings,
apiKey: input.settings.apiKey,
request: retryRequestPlan.request,
...(input.onStreamDelta ? { onStreamDelta: input.onStreamDelta } : {}),
});
}
}

async function callGeminiInteractionWithOptionalStreaming(input: {
settings: GeminiSettings;
apiKey: string;
request: unknown;
onStreamDelta?: (delta: GeminiStreamDelta) => void;
}): Promise<Awaited<ReturnType<typeof callGeminiInteraction>>> {
if (input.onStreamDelta) {
return callGeminiInteractionStream({
settings: input.settings,
apiKey: input.apiKey,
request: input.request,
onStreamDelta: input.onStreamDelta,
});
}

return callGeminiInteraction({
settings: input.settings,
apiKey: input.apiKey,
request: input.request,
});
}
Expand Down Expand Up @@ -346,23 +345,17 @@ export async function generateSessionTitle(
})),
];

const client = getGeminiClient(normalizedApiKey);
const response = (await client.interactions.create({
model: SESSION_TITLE_MODEL,
input,
store: false,
} as unknown as SDKCreateInteractionRequest)) as unknown;

if (!isRecord(response)) {
throw new Error('Gemini title generation response payload was not a JSON object.');
}
const interaction = await callGeminiInteraction({
apiKey: normalizedApiKey,
request: {
model: SESSION_TITLE_MODEL,
input,
store: false,
},
});

const rawOutputs = Array.isArray(response.outputs) ? response.outputs : [];
const rawOutputs = interaction.outputs ?? [];
for (const rawOutput of rawOutputs) {
if (!isRecord(rawOutput)) {
continue;
}

if (rawOutput.type !== 'text' || typeof rawOutput.text !== 'string') {
continue;
}
Expand Down
9 changes: 4 additions & 5 deletions src/background/features/gemini/gemini/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { GeminiSettings } from '../../../../shared/settings';
import { isRecord } from '../../../core/utils';
import { getGeminiClient } from '../gemini-client';
import { readStringField } from './common';
Expand All @@ -22,15 +21,15 @@ import {
} from './streaming';

export async function callGeminiInteraction(input: {
settings: GeminiSettings;
apiKey: string;
request: unknown;
}): Promise<GeminiInteraction> {
const response = await createInteraction(input.settings.apiKey, input.request);
const response = await createInteraction(input.apiKey, input.request);
return normalizeGeminiInteractionResponse(response);
}

export async function callGeminiInteractionStream(input: {
settings: GeminiSettings;
apiKey: string;
request: unknown;
onStreamDelta: (delta: GeminiStreamDelta) => void;
}): Promise<GeminiInteraction> {
Expand All @@ -39,7 +38,7 @@ export async function callGeminiInteractionStream(input: {
stream: true,
};

const stream = await createInteraction(input.settings.apiKey, streamRequest);
const stream = await createInteraction(input.apiKey, streamRequest);

if (!isAsyncIterable(stream)) {
throw new Error('Gemini streaming response payload was not an async iterable.');
Expand Down
2 changes: 1 addition & 1 deletion src/background/features/gemini/gemini/title.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const MAX_SESSION_TITLE_LENGTH = 60;
export const SESSION_TITLE_MODEL = 'gemini-flash-lite-latest';
export const SESSION_TITLE_MODEL = 'gemini-3.1-flash-lite-preview';

export function buildSessionTitlePrompt(firstUserQuery: string): string {
const lines = [
Expand Down
24 changes: 0 additions & 24 deletions src/chatpanel/features/composer/input-toolbar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
DEFAULT_GEMINI_MODEL,
GEMINI_SETTINGS_STORAGE_KEY,
getModelDisplayLabel,
getModelThinkingLevels,
normalizeGeminiSettings,
} from '../../../shared/settings';
Expand Down Expand Up @@ -115,28 +114,6 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar {
);
}

function applyCustomModels(customModels: string[]): void {
const existing = new Set<string>();
for (const item of Array.from(modelMenu.querySelectorAll('.dropup-item'))) {
if ((item as HTMLElement).dataset.custom) {
item.remove();
} else {
existing.add(item.getAttribute('data-value') ?? '');
}
}
for (const model of customModels) {
if (!existing.has(model)) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'dropup-item';
btn.dataset.value = model;
btn.dataset.custom = '1';
btn.textContent = getModelDisplayLabel(model);
modelMenu.appendChild(btn);
}
}
}

modelTrigger.addEventListener('click', (e) => {
e.stopPropagation();
const wasOpen = modelMenuController.isOpen();
Expand Down Expand Up @@ -185,7 +162,6 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar {
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);
}
Expand Down
3 changes: 3 additions & 0 deletions src/chatpanel/features/conversation/conversation-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export function createConversationFlowController(
id: draft.assistantMessageId,
role: 'assistant',
content: draft.text,
isStreaming: true,
};
if (draft.thinkingSummary) {
streamMessage.thinkingSummary = draft.thinkingSummary;
Expand Down Expand Up @@ -297,6 +298,7 @@ export function createConversationFlowController(
id: assistantPlaceholderId,
role: 'assistant',
content: '',
isStreaming: true,
});

if (input.shouldResetComposerText) {
Expand Down Expand Up @@ -377,6 +379,7 @@ export function createConversationFlowController(
id: regenPlaceholderMessageId,
role: 'assistant',
content: '',
isStreaming: true,
});

regenStreamRequestId = crypto.randomUUID();
Expand Down
4 changes: 4 additions & 0 deletions src/chatpanel/features/messages/message-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ function createAssistantActionBar(
return null;
}

if (message.isStreaming) {
return null;
}

const hasRegenerateAction = !!message.interactionId?.trim() && !!options.onAssistantAction;
const branchOptions = message.branchOptionInteractionIds ?? [];
const hasBranchSwitchAction = branchOptions.length > 1 && !!options.onAssistantBranchSelect;
Expand Down
6 changes: 4 additions & 2 deletions src/chatpanel/template/composer-template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
BUILTIN_GEMINI_MODEL_CATALOG,
type BuiltinGeminiModelKey,
type ThinkingLevel,
getBuiltinGeminiModelByKey,
} from '../../shared/settings';
Expand All @@ -10,9 +10,11 @@ const THINKING_LABELS: Record<ThinkingLevel, string> = {
low: 'Low',
minimal: 'Min',
};
const MODEL_MENU_ORDER: readonly BuiltinGeminiModelKey[] = ['pro', 'flash', 'flash-lite'];

function renderModelMenuItems(selectedModel: string): string {
return BUILTIN_GEMINI_MODEL_CATALOG.map((entry) => {
return MODEL_MENU_ORDER.map((key) => {
const entry = getBuiltinGeminiModelByKey(key);
const selected = entry.model === selectedModel ? ' aria-selected="true"' : '';
return `<button type="button" class="dropup-item" data-value="${entry.model}"${selected}>${entry.label}</button>`;
}).join('');
Expand Down
12 changes: 6 additions & 6 deletions src/options/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ export interface OptionsDom {
apiKeyInput: HTMLInputElement;
modelFlashNameInput: HTMLInputElement;
modelFlashThinkingLevelSelect: HTMLSelectElement;
modelFlashLiteNameInput: HTMLInputElement;
modelFlashLiteThinkingLevelSelect: HTMLSelectElement;
modelProNameInput: HTMLInputElement;
modelProThinkingLevelSelect: HTMLSelectElement;
customModelRowsContainer: HTMLElement;
addCustomModelButton: HTMLButtonElement;
customModelRowTemplate: HTMLTemplateElement;
systemInstructionInput: HTMLTextAreaElement;
storeInteractionsInput: HTMLInputElement;
maxToolRoundTripsInput: HTMLInputElement;
Expand Down Expand Up @@ -39,13 +38,14 @@ export function getOptionsDom(): OptionsDom {
modelFlashThinkingLevelSelect: queryRequiredElement<HTMLSelectElement>(
'#model-thinking-level-flash',
),
modelFlashLiteNameInput: queryRequiredElement<HTMLInputElement>('#model-name-flash-lite'),
modelFlashLiteThinkingLevelSelect: queryRequiredElement<HTMLSelectElement>(
'#model-thinking-level-flash-lite',
),
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