From baea491cf2f8e9c7e0d8af43d0d5a7a15a27ff19 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Sat, 21 Mar 2026 20:05:54 +0100 Subject: [PATCH] feat: add model dropdown with provider logos and tier badges - Create model catalog (src/shared/domain/models.ts) with 12 models from OpenAI, Anthropic, and Google including tier metadata - Replace free-text model input with custom ModelSelect dropdown grouped by provider with colored provider icons (green AI for OpenAI, orange A for Anthropic, blue G for Google) - Show tier badges (premium/standard/fast) on each model option - Highlight currently selected model with indigo accent - Update ActivityPanel to show provider icons on model badges for visual consistency across the app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/renderer/components/ActivityPanel.tsx | 22 +++- src/renderer/components/PatternEditor.tsx | 119 +++++++++++++++++++++- src/shared/domain/models.ts | 42 ++++++++ 3 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/shared/domain/models.ts diff --git a/src/renderer/components/ActivityPanel.tsx b/src/renderer/components/ActivityPanel.tsx index 2a11102..9f4195a 100644 --- a/src/renderer/components/ActivityPanel.tsx +++ b/src/renderer/components/ActivityPanel.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Activity, Bot, Cpu, Sparkles } from 'lucide-react'; +import { Activity, Bot, Sparkles } from 'lucide-react'; import { buildAgentActivityRows, @@ -8,9 +8,16 @@ import { isAgentActivityCompleted, type SessionActivityState, } from '@renderer/lib/sessionActivity'; +import { inferProvider, type ModelProvider } from '@shared/domain/models'; import type { PatternDefinition } from '@shared/domain/pattern'; import type { SessionRecord } from '@shared/domain/session'; +const providerStyles: Record = { + openai: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', label: 'AI' }, + anthropic: { bg: 'bg-orange-500/15', text: 'text-orange-400', label: 'A' }, + google: { bg: 'bg-blue-500/15', text: 'text-blue-400', label: 'G' }, +}; + function formatModel(model: string): string { return model.replace(/-/g, '\u2011'); } @@ -92,7 +99,18 @@ export function ActivityPanel({ activity, pattern, session }: ActivityPanelProps {agent && (
- + {(() => { + const prov = inferProvider(agent.model); + if (prov) { + const s = providerStyles[prov]; + return ( + + {s.label} + + ); + } + return null; + })()} {formatModel(agent.model)} {agent.reasoningEffort && ( diff --git a/src/renderer/components/PatternEditor.tsx b/src/renderer/components/PatternEditor.tsx index e8fcbb0..164931c 100644 --- a/src/renderer/components/PatternEditor.tsx +++ b/src/renderer/components/PatternEditor.tsx @@ -1,8 +1,9 @@ -import { useState, type ReactNode } from 'react'; +import { useEffect, useRef, useState, type ReactNode } from 'react'; import { AlertCircle, ArrowLeftRight, CheckCircle, + ChevronDown, ChevronLeft, GitFork, ListOrdered, @@ -21,6 +22,14 @@ import { type ReasoningEffort, type PatternAgentDefinition, } from '@shared/domain/pattern'; +import { + modelCatalog, + providerMeta, + findModel, + inferProvider, + type ModelProvider, + type ModelDefinition, +} from '@shared/domain/models'; interface PatternEditorProps { pattern: PatternDefinition; @@ -194,6 +203,110 @@ function InputField({ ); } +const providerStyles: Record = { + openai: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', label: 'AI' }, + anthropic: { bg: 'bg-orange-500/15', text: 'text-orange-400', label: 'A' }, + google: { bg: 'bg-blue-500/15', text: 'text-blue-400', label: 'G' }, +}; + +function ProviderIcon({ provider }: { provider: ModelProvider }) { + const s = providerStyles[provider]; + return ( + + {s.label} + + ); +} + +function TierBadge({ tier }: { tier: ModelDefinition['tier'] }) { + const styles = { + premium: 'bg-amber-500/10 text-amber-400', + standard: 'bg-zinc-700/50 text-zinc-500', + fast: 'bg-emerald-500/10 text-emerald-400', + }; + return ( + + {tier} + + ); +} + +function ModelSelect({ value, onChange }: { value: string; onChange: (model: string) => void }) { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + + const selected = findModel(value); + const provider = selected?.provider ?? inferProvider(value); + + return ( + + ); +} + export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave, onBack }: PatternEditorProps) { const issues = validatePatternDefinition(pattern); @@ -412,10 +525,8 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave, onChange={(v) => updateAgent(agent.id, { name: v })} value={agent.name} /> - updateAgent(agent.id, { model: v })} - placeholder="e.g. gpt-5.4" value={agent.model} />