From d26b14e873676bc74e9433b9a1d7c8441ccc3e92 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:26:02 +0530 Subject: [PATCH 1/4] let model switch --- .../components/chat-input-with-mentions.tsx | 162 +++++++++++++++--- .../src/components/settings-dialog.tsx | 26 ++- apps/x/packages/core/src/models/repo.ts | 20 ++- 3 files changed, 178 insertions(+), 30 deletions(-) diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index d3554c009..29b64da1a 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { ArrowUp, AudioLines, + ChevronDown, FileArchive, FileCode2, FileIcon, @@ -15,6 +16,13 @@ import { } from 'lucide-react' import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { type AttachmentIconKind, getAttachmentDisplayName, @@ -45,6 +53,25 @@ export type StagedAttachment = { const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024 // 10MB +const providerDisplayNames: Record = { + openai: 'OpenAI', + anthropic: 'Anthropic', + google: 'Gemini', + ollama: 'Ollama', + openrouter: 'OpenRouter', + aigateway: 'AI Gateway', + 'openai-compatible': 'OpenAI-Compatible', +} + +interface ConfiguredModel { + flavor: string + model: string + apiKey?: string + baseURL?: string + headers?: Record + knowledgeGraphModel?: string +} + function getAttachmentIcon(kind: AttachmentIconKind) { switch (kind) { case 'audio': @@ -96,6 +123,62 @@ function ChatInputInner({ const fileInputRef = useRef(null) const canSubmit = (Boolean(message.trim()) || attachments.length > 0) && !isProcessing + const [configuredModels, setConfiguredModels] = useState([]) + const [activeModelKey, setActiveModelKey] = useState('') + + // Load model config from disk + useEffect(() => { + async function loadModels() { + try { + const result = await window.ipc.invoke('workspace:readFile', { path: 'config/models.json' }) + const parsed = JSON.parse(result.data) + const models: ConfiguredModel[] = [] + if (parsed?.providers) { + for (const [flavor, entry] of Object.entries(parsed.providers)) { + const e = entry as Record + if (e.model && typeof e.model === 'string') { + models.push({ + flavor, + model: e.model, + apiKey: (e.apiKey as string) || undefined, + baseURL: (e.baseURL as string) || undefined, + headers: (e.headers as Record) || undefined, + knowledgeGraphModel: (e.knowledgeGraphModel as string) || undefined, + }) + } + } + } + setConfiguredModels(models) + if (parsed?.provider?.flavor && parsed?.model) { + setActiveModelKey(`${parsed.provider.flavor}/${parsed.model}`) + } + } catch { + // No config yet + } + } + loadModels() + }, []) + + const handleModelChange = useCallback(async (key: string) => { + const entry = configuredModels.find((m) => `${m.flavor}/${m.model}` === key) + if (!entry) return + setActiveModelKey(key) + try { + await window.ipc.invoke('models:saveConfig', { + provider: { + flavor: entry.flavor, + apiKey: entry.apiKey, + baseURL: entry.baseURL, + headers: entry.headers, + }, + model: entry.model, + knowledgeGraphModel: entry.knowledgeGraphModel, + }) + } catch { + toast.error('Failed to switch model') + } + }, [configuredModels]) + // Restore the tab draft when this input mounts. useEffect(() => { if (initialDraft) { @@ -239,24 +322,33 @@ function ChatInputInner({ })} )} -
- { - const files = e.target.files - if (!files || files.length === 0) return - const paths = Array.from(files) - .map((file) => window.electronUtils?.getPathForFile(file)) - .filter(Boolean) as string[] - if (paths.length > 0) { - void addFiles(paths) - } - e.target.value = '' - }} + { + const files = e.target.files + if (!files || files.length === 0) return + const paths = Array.from(files) + .map((file) => window.electronUtils?.getPathForFile(file)) + .filter(Boolean) as string[] + if (paths.length > 0) { + void addFiles(paths) + } + e.target.value = '' + }} + /> +
+ +
+
- +
+ {configuredModels.length > 0 && ( + + + + + + + {configuredModels.map((m) => { + const key = `${m.flavor}/${m.model}` + return ( + + {m.model} + {providerDisplayNames[m.flavor] || m.flavor} + + ) + })} + + + + )} {isProcessing ? ( + )} +
+ ))} + +
)} {modelsError && (
{modelsError}
)}
+ {/* Knowledge graph model (right column) */}
Knowledge graph model {modelsLoading ? ( @@ -428,7 +497,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { updateConfig(provider, { knowledgeGraphModel: e.target.value })} - placeholder={activeConfig.model || "Enter model"} + placeholder={primaryModel || "Enter model"} /> ) : (