fix: first round of fixes of implementation

This commit is contained in:
2026-03-01 15:11:33 +01:00
parent c911ec2354
commit 202ea1b7cc
7 changed files with 233 additions and 37 deletions

View File

@@ -252,6 +252,9 @@ export class OpenCodeManager {
*/
setApiKey(key: string): void {
this.apiKey = key;
// Invalidate model cache so merged list is re-fetched
this.cachedModels = null;
this.cachedModelsAt = 0;
}
/**
@@ -316,7 +319,7 @@ export class OpenCodeManager {
// Filter to only OpenCode models (not Mistral)
const models = Object.entries(MODEL_DISPLAY_NAMES)
.map(([id, name]) => ({ id, name, provider: this.detectProvider(id), vision: MODEL_CAPABILITIES[id]?.vision ?? false }))
.filter(m => this.isProviderKeySet(m.provider) || m.provider !== 'mistral');
.filter(m => this.isProviderKeySet(m.provider));
return { isValid: true, models };
}
} catch {
@@ -2062,13 +2065,14 @@ export class OpenCodeManager {
_assistantResponse: string
): Promise<void> {
try {
// Read configured title model
const titleModel = await this.chatEngine.getSetting('chat_title_model') || 'claude-haiku-4-5';
// Read configured title model, with smart fallback based on available keys
let titleModel = await this.chatEngine.getSetting('chat_title_model');
if (!titleModel || !this.isProviderKeySet(this.detectProvider(titleModel))) {
titleModel = this.apiKey ? 'claude-haiku-4-5' : this.mistralApiKey ? 'mistral-small-latest' : null;
}
if (!titleModel) return;
const provider = this.detectProvider(titleModel);
// Ensure we have the key for this provider
if (!this.isProviderKeySet(provider)) return;
const promptText = `Topic: ${userMessage.substring(0, 100)}`;
const systemText = 'Generate an ultra-short title (2-3 words, max 25 characters) for this conversation. Focus ONLY on the topic. Ignore any capability disclaimers. Output ONLY the title text.';
@@ -2492,14 +2496,15 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
caption?: string;
error?: string;
}> {
// Read configured image analysis model (default: claude-sonnet-4-5)
const modelId = await this.chatEngine.getSetting('chat_image_analysis_model') || 'claude-sonnet-4-5';
const provider = this.detectProvider(modelId);
if (!this.isProviderKeySet(provider)) {
const providerLabel = provider === 'mistral' ? 'Mistral' : 'OpenCode';
return { success: false, error: `API key not configured. Please set your ${providerLabel} API key in Settings.` };
// Read configured image analysis model, with smart fallback based on available keys
let modelId = await this.chatEngine.getSetting('chat_image_analysis_model');
if (!modelId || !this.isProviderKeySet(this.detectProvider(modelId))) {
modelId = this.apiKey ? 'claude-sonnet-4-5' : this.mistralApiKey ? 'mistral-large-latest' : null;
}
if (!modelId) {
return { success: false, error: 'API key not configured. Please set an API key in Settings.' };
}
const provider = this.detectProvider(modelId);
// Get media metadata
const mediaItem = await this.mediaEngine.getMedia(mediaId);

View File

@@ -228,10 +228,10 @@ export function registerChatHandlers(): void {
try {
const engine = getChatEngine();
const model = await engine.getSetting('chat_title_model');
return { success: true, modelId: model || 'claude-haiku-4-5' };
return { success: true, modelId: model || null };
} catch (error) {
console.error('[Chat IPC] Error getting title model:', error);
return { success: false, modelId: 'claude-haiku-4-5' };
return { success: false, modelId: null };
}
});
@@ -252,10 +252,10 @@ export function registerChatHandlers(): void {
try {
const engine = getChatEngine();
const model = await engine.getSetting('chat_image_analysis_model');
return { success: true, modelId: model || 'claude-sonnet-4-5' };
return { success: true, modelId: model || null };
} catch (error) {
console.error('[Chat IPC] Error getting image analysis model:', error);
return { success: false, modelId: 'claude-sonnet-4-5' };
return { success: false, modelId: null };
}
});

View File

@@ -366,7 +366,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
return Object.entries(groups).map(([provider, models]) => (
<React.Fragment key={provider}>
{Object.keys(groups).length > 1 && (
<div className="model-group-header">{provider === 'mistral' ? 'Mistral' : 'OpenCode'}</div>
<div className="model-group-header">{provider === 'mistral' ? tr('settings.ai.providerMistral') : tr('settings.ai.providerOpenCode')}</div>
)}
{models.map(model => (
<button

View File

@@ -1381,7 +1381,7 @@ const TaxonomySection: React.FC<{
return Object.entries(groups).map(([provider, models]) => (
<React.Fragment key={provider}>
{Object.keys(groups).length > 1 && (
<div className="model-group-header">{provider === 'mistral' ? 'Mistral' : 'OpenCode'}</div>
<div className="model-group-header">{provider === 'mistral' ? t('settings.ai.providerMistral') : t('settings.ai.providerOpenCode')}</div>
)}
{models.map(model => (
<button

View File

@@ -1215,15 +1215,23 @@ export const SettingsView: React.FC = () => {
};
// Group models by provider for optgroup display
const groupedModels = useMemo(() => {
const groupModelsByProvider = useCallback((models: typeof availableModels) => {
const groups: Record<string, typeof availableModels> = {};
for (const model of availableModels) {
for (const model of models) {
const provider = model.provider || 'other';
if (!groups[provider]) groups[provider] = [];
groups[provider].push(model);
}
return groups;
}, [availableModels]);
}, []);
const groupedModels = useMemo(() => groupModelsByProvider(availableModels), [availableModels, groupModelsByProvider]);
// Vision-capable models only (for image analysis model selector)
const groupedVisionModels = useMemo(
() => groupModelsByProvider(availableModels.filter(m => m.vision)),
[availableModels, groupModelsByProvider]
);
const providerLabel = (provider: string) => {
if (provider === 'anthropic' || provider === 'openai' || provider === 'google' || provider === 'other') return t('settings.ai.providerOpenCode');
@@ -1232,18 +1240,22 @@ export const SettingsView: React.FC = () => {
};
// Render a model <select> with optgroup by provider
const renderModelSelect = (id: string, value: string, onChange: (v: string) => void, disabled?: boolean) => (
<select id={id} value={value} onChange={(e) => onChange(e.target.value)} disabled={disabled}>
{availableModels.length === 0 && <option value="">{t('settings.ai.noModels')}</option>}
{Object.entries(groupedModels).map(([provider, models]) => (
<optgroup key={provider} label={providerLabel(provider)}>
{models.map(model => (
<option key={model.id} value={model.id}>{model.name}</option>
))}
</optgroup>
))}
</select>
);
const renderModelSelect = (id: string, value: string, onChange: (v: string) => void, disabled?: boolean, groups?: Record<string, typeof availableModels>) => {
const modelGroups = groups || groupedModels;
const modelList = Object.values(modelGroups).flat();
return (
<select id={id} value={value} onChange={(e) => onChange(e.target.value)} disabled={disabled}>
{modelList.length === 0 && <option value="">{t('settings.ai.noModels')}</option>}
{Object.entries(modelGroups).map(([provider, models]) => (
<optgroup key={provider} label={providerLabel(provider)}>
{models.map(model => (
<option key={model.id} value={model.id}>{model.name}</option>
))}
</optgroup>
))}
</select>
);
};
const renderAISettings = () => (
<SettingSection
@@ -1384,7 +1396,7 @@ export const SettingsView: React.FC = () => {
label={t('settings.ai.imageAnalysisModelLabel')}
description={t('settings.ai.imageAnalysisModelDescription')}
>
{renderModelSelect('ai-image-analysis-model', imageAnalysisModel, handleImageAnalysisModelChange, !aiHasApiKey && !aiHasMistralKey)}
{renderModelSelect('ai-image-analysis-model', imageAnalysisModel, handleImageAnalysisModelChange, !aiHasApiKey && !aiHasMistralKey, groupedVisionModels)}
</SettingRow>
<SettingRow

View File

@@ -734,7 +734,7 @@
"settings.ai.modelInfoTokens": "Token",
"settings.ai.modelInfoPerMTok": "/MTok",
"settings.ai.mistralApiKeyLabel": "Mistral API-Schlüssel",
"settings.ai.mistralApiKeyDescription": "Ihr API-Schlüssel von Mistral AI. Ermöglicht Mistral-Modelle als Alternative zu OpenCode.",
"settings.ai.mistralApiKeyDescription": "Dein API-Schlüssel von Mistral AI. Ermöglicht Mistral-Modelle als Alternative zu OpenCode.",
"settings.ai.mistralApiKeyConfigured": "Mistral API-Schlüssel konfiguriert",
"settings.ai.changeMistralApiKey": "Mistral API-Schlüssel ändern",
"settings.ai.titleModelLabel": "Titelgenerierungsmodell",
@@ -744,7 +744,7 @@
"settings.ai.providerOpenCode": "OpenCode",
"settings.ai.providerMistral": "Mistral",
"settings.ai.providerOther": "Andere",
"chat.providerKeyMissing": "Das Modell '{{model}}' benötigt einen {{provider}} API-Schlüssel. Konfigurieren Sie ihn in den Einstellungen.",
"chat.providerKeyMissing": "Das Modell '{{model}}' benötigt einen {{provider}} API-Schlüssel. Konfiguriere ihn in den Einstellungen.",
"settings.toast.modelCatalogRefreshed": "Modellkatalog aktualisiert ({{count}} Modelle)",
"settings.toast.modelCatalogUpToDate": "Modellkatalog ist bereits aktuell",
"settings.toast.modelCatalogRefreshFailed": "Modellkatalog konnte nicht aktualisiert werden",