diff --git a/src/renderer/components/ChatPane.tsx b/src/renderer/components/ChatPane.tsx index 4945bdd..c340f97 100644 --- a/src/renderer/components/ChatPane.tsx +++ b/src/renderer/components/ChatPane.tsx @@ -1,16 +1,18 @@ import { type KeyboardEvent, useEffect, useRef, useState } from 'react'; -import { AlertCircle, ArrowUp, Bot, Loader2, User } from 'lucide-react'; +import { AlertCircle, ArrowUp, Bot, ChevronDown, Loader2, Sparkles, User } from 'lucide-react'; -import { ModelSelect, ReasoningEffortSelect } from '@renderer/components/AgentConfigFields'; import { MarkdownContent } from '@renderer/components/MarkdownContent'; import { getAssistantMessagePhase } from '@renderer/lib/messagePhase'; +import { ProviderIcon } from '@renderer/components/ProviderIcons'; import { findModel, getSupportedReasoningEfforts, + inferProvider, + providerMeta, resolveReasoningEffort, type ModelDefinition, } from '@shared/domain/models'; -import type { PatternDefinition, ReasoningEffort } from '@shared/domain/pattern'; +import { reasoningEffortOptions, type PatternDefinition, type ReasoningEffort } from '@shared/domain/pattern'; import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project'; import type { SessionRecord } from '@shared/domain/session'; @@ -24,6 +26,203 @@ function ThinkingDots() { ); } +/* ── Tier badge for model dropdown ─────────────────────────── */ + +function TierBadge({ tier }: { tier: ModelDefinition['tier'] }) { + if (!tier) return null; + 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} + + ); +} + +/* ── Inline model pill with dropdown ───────────────────────── */ + +function InlineModelPill({ + value, + models, + onChange, + disabled, +}: { + value: string; + models: ReadonlyArray; + onChange: (model: string) => void; + disabled: boolean; +}) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + + const selected = findModel(value, models); + const provider = selected?.provider ?? inferProvider(value); + const displayName = selected?.name ?? value ?? 'Model'; + + const groupedModels = providerMeta + .map((pg) => ({ ...pg, models: models.filter((m) => m.provider === pg.id) })) + .filter((pg) => pg.models.length > 0); + const otherModels = models.filter((m) => !m.provider); + + return ( +
+ + + {open && !disabled && ( +
+ {groupedModels.map((pg) => ( +
+
+ + + {pg.label} + +
+ {pg.models.map((model) => ( + + ))} +
+ ))} + {otherModels.length > 0 && ( +
+
+ Other +
+ {otherModels.map((model) => ( + + ))} +
+ )} +
+ )} +
+ ); +} + +/* ── Inline thinking effort pill with dropdown ─────────────── */ + +function InlineThinkingPill({ + value, + supportedEfforts, + onChange, + disabled, +}: { + value?: ReasoningEffort; + supportedEfforts?: ReadonlyArray; + onChange: (effort: ReasoningEffort) => void; + disabled: boolean; +}) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + + const options = supportedEfforts + ? reasoningEffortOptions.filter((o) => supportedEfforts.includes(o.value)) + : [...reasoningEffortOptions]; + + if (supportedEfforts && supportedEfforts.length === 0) { + return ( + + + N/A + + ); + } + + const currentLabel = options.find((o) => o.value === value)?.label ?? value ?? 'Thinking'; + + return ( +
+ + + {open && !disabled && ( +
+ {options.map((option) => ( + + ))} +
+ )} +
+ ); +} + +/* ── ChatPane ──────────────────────────────────────────────── */ + interface ChatPaneProps { project: ProjectRecord; pattern: PatternDefinition; @@ -113,6 +312,7 @@ export function ChatPane({ return (
+ {/* Header — extra top padding clears the title bar overlay zone */}

{session.title}

@@ -136,6 +336,7 @@ export function ChatPane({
+ {/* Messages */}
{session.messages.length === 0 ? (
@@ -218,6 +419,7 @@ export function ChatPane({ )}
+ {/* Input area */}
{session.lastError && (
@@ -234,41 +436,35 @@ export function ChatPane({ )}
+ {/* Scratchpad config pills — inline above composer */} {isScratchpad && primaryAgent && ( -
-
-
- { - const nextModel = findModel(modelId, availableModels); - void handleScratchpadConfigChange({ - model: modelId, - reasoningEffort: resolveReasoningEffort(nextModel, scratchpadReasoningEffort), - }); - }} - value={primaryAgent.model} - /> -
-
- - void handleScratchpadConfigChange({ - model: primaryAgent.model, - reasoningEffort, - }) - } - supportedEfforts={supportedEfforts} - value={scratchpadReasoningEffort} - /> -
-
-

- Applies to future replies in this scratchpad. -

+
+ { + const nextModel = findModel(modelId, availableModels); + void handleScratchpadConfigChange({ + model: modelId, + reasoningEffort: resolveReasoningEffort(nextModel, scratchpadReasoningEffort), + }); + }} + value={primaryAgent.model} + /> + + void handleScratchpadConfigChange({ + model: primaryAgent.model, + reasoningEffort, + }) + } + supportedEfforts={supportedEfforts} + value={scratchpadReasoningEffort} + /> + {isUpdatingScratchpadConfig && ( + + )}
)}