mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
feat: integrate thinking level support across agent and UI components
- Enhanced the agent service and request handling to include an optional thinking level parameter, improving the configurability of model interactions. - Updated the UI components to manage and display the selected model along with its thinking level, ensuring a cohesive user experience. - Refactored the model selector and input controls to accommodate the new model selection structure, enhancing usability and clarity. - Adjusted the Electron API and HTTP client to support the new thinking level parameter in requests, ensuring consistent data handling across the application. This update significantly improves the agent's ability to adapt its reasoning capabilities based on user-defined thinking levels, enhancing overall performance and user satisfaction.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useAppStore, type ModelAlias } from '@/store/app-store';
|
||||
import type { CursorModelId } from '@automaker/types';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { PhaseModelEntry } from '@automaker/types';
|
||||
import { useElectronAgent } from '@/hooks/use-electron-agent';
|
||||
import { SessionManager } from '@/components/session-manager';
|
||||
|
||||
@@ -21,7 +21,7 @@ export function AgentView() {
|
||||
const [input, setInput] = useState('');
|
||||
const [currentTool, setCurrentTool] = useState<string | null>(null);
|
||||
const [showSessionManager, setShowSessionManager] = useState(true);
|
||||
const [selectedModel, setSelectedModel] = useState<ModelAlias | CursorModelId>('sonnet');
|
||||
const [modelSelection, setModelSelection] = useState<PhaseModelEntry>({ model: 'sonnet' });
|
||||
|
||||
// Input ref for auto-focus
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -50,7 +50,8 @@ export function AgentView() {
|
||||
} = useElectronAgent({
|
||||
sessionId: currentSessionId || '',
|
||||
workingDirectory: currentProject?.path,
|
||||
model: selectedModel,
|
||||
model: modelSelection.model,
|
||||
thinkingLevel: modelSelection.thinkingLevel,
|
||||
onToolUse: (toolName) => {
|
||||
setCurrentTool(toolName);
|
||||
setTimeout(() => setCurrentTool(null), 2000);
|
||||
@@ -185,8 +186,8 @@ export function AgentView() {
|
||||
onInputChange={setInput}
|
||||
onSend={handleSend}
|
||||
onStop={stopExecution}
|
||||
selectedModel={selectedModel}
|
||||
onModelSelect={setSelectedModel}
|
||||
modelSelection={modelSelection}
|
||||
onModelSelect={setModelSelection}
|
||||
isProcessing={isProcessing}
|
||||
isConnected={isConnected}
|
||||
selectedImages={fileAttachments.selectedImages}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ImageDropZone } from '@/components/ui/image-drop-zone';
|
||||
import type { ImageAttachment, TextFileAttachment, ModelAlias } from '@/store/app-store';
|
||||
import type { CursorModelId } from '@automaker/types';
|
||||
import type { ImageAttachment, TextFileAttachment } from '@/store/app-store';
|
||||
import type { PhaseModelEntry } from '@automaker/types';
|
||||
import { FilePreview } from './file-preview';
|
||||
import { QueueDisplay } from './queue-display';
|
||||
import { InputControls } from './input-controls';
|
||||
@@ -16,8 +16,10 @@ interface AgentInputAreaProps {
|
||||
onInputChange: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onStop: () => void;
|
||||
selectedModel: ModelAlias | CursorModelId;
|
||||
onModelSelect: (model: ModelAlias | CursorModelId) => void;
|
||||
/** Current model selection (model + optional thinking level) */
|
||||
modelSelection: PhaseModelEntry;
|
||||
/** Callback when model is selected */
|
||||
onModelSelect: (entry: PhaseModelEntry) => void;
|
||||
isProcessing: boolean;
|
||||
isConnected: boolean;
|
||||
// File attachments
|
||||
@@ -48,7 +50,7 @@ export function AgentInputArea({
|
||||
onInputChange,
|
||||
onSend,
|
||||
onStop,
|
||||
selectedModel,
|
||||
modelSelection,
|
||||
onModelSelect,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
@@ -113,7 +115,7 @@ export function AgentInputArea({
|
||||
onStop={onStop}
|
||||
onToggleImageDropZone={onToggleImageDropZone}
|
||||
onPaste={onPaste}
|
||||
selectedModel={selectedModel}
|
||||
modelSelection={modelSelection}
|
||||
onModelSelect={onModelSelect}
|
||||
isProcessing={isProcessing}
|
||||
isConnected={isConnected}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AgentModelSelector } from '../shared/agent-model-selector';
|
||||
import type { ModelAlias } from '@/store/app-store';
|
||||
import type { CursorModelId } from '@automaker/types';
|
||||
import type { PhaseModelEntry } from '@automaker/types';
|
||||
|
||||
interface InputControlsProps {
|
||||
input: string;
|
||||
@@ -14,8 +13,10 @@ interface InputControlsProps {
|
||||
onStop: () => void;
|
||||
onToggleImageDropZone: () => void;
|
||||
onPaste: (e: React.ClipboardEvent) => Promise<void>;
|
||||
selectedModel: ModelAlias | CursorModelId;
|
||||
onModelSelect: (model: ModelAlias | CursorModelId) => void;
|
||||
/** Current model selection (model + optional thinking level) */
|
||||
modelSelection: PhaseModelEntry;
|
||||
/** Callback when model is selected */
|
||||
onModelSelect: (entry: PhaseModelEntry) => void;
|
||||
isProcessing: boolean;
|
||||
isConnected: boolean;
|
||||
hasFiles: boolean;
|
||||
@@ -37,7 +38,7 @@ export function InputControls({
|
||||
onStop,
|
||||
onToggleImageDropZone,
|
||||
onPaste,
|
||||
selectedModel,
|
||||
modelSelection,
|
||||
onModelSelect,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
@@ -125,8 +126,8 @@ export function InputControls({
|
||||
|
||||
{/* Model Selector */}
|
||||
<AgentModelSelector
|
||||
selectedModel={selectedModel}
|
||||
onModelSelect={onModelSelect}
|
||||
value={modelSelection}
|
||||
onChange={onModelSelect}
|
||||
disabled={!isConnected}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,127 +1,25 @@
|
||||
import { ChevronDown, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore, type ModelAlias } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||
import type { CursorModelId } from '@automaker/types';
|
||||
import { getModelProvider, stripProviderPrefix } from '@automaker/types';
|
||||
/**
|
||||
* Re-export PhaseModelSelector in compact mode for use in agent chat view.
|
||||
* This ensures we have a single source of truth for model selection logic.
|
||||
*/
|
||||
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/phase-models/phase-model-selector';
|
||||
import type { PhaseModelEntry } from '@automaker/types';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { PhaseModelEntry };
|
||||
|
||||
interface AgentModelSelectorProps {
|
||||
selectedModel: ModelAlias | CursorModelId;
|
||||
onModelSelect: (model: ModelAlias | CursorModelId) => void;
|
||||
/** Current model selection (model + optional thinking level) */
|
||||
value: PhaseModelEntry;
|
||||
/** Callback when model is selected */
|
||||
onChange: (entry: PhaseModelEntry) => void;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AgentModelSelector({
|
||||
selectedModel,
|
||||
onModelSelect,
|
||||
disabled,
|
||||
}: AgentModelSelectorProps) {
|
||||
const { enabledCursorModels } = useAppStore();
|
||||
const { cursorCliStatus } = useSetupStore();
|
||||
|
||||
// Check if Cursor CLI is available
|
||||
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
|
||||
|
||||
// Filter cursor models by enabled settings
|
||||
const filteredCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
const modelId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
return enabledCursorModels.includes(modelId);
|
||||
});
|
||||
|
||||
// Determine current provider and display label
|
||||
const currentProvider = getModelProvider(selectedModel);
|
||||
const currentModel =
|
||||
currentProvider === 'cursor'
|
||||
? CURSOR_MODELS.find((m) => m.id === selectedModel)
|
||||
: CLAUDE_MODELS.find((m) => m.id === selectedModel);
|
||||
|
||||
// Get display label (strip "Claude " prefix for brevity)
|
||||
const displayLabel = currentModel?.label.replace('Claude ', '') || 'Sonnet';
|
||||
|
||||
export function AgentModelSelector({ value, onChange, disabled }: AgentModelSelectorProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 gap-1 text-xs font-medium rounded-xl border-border px-2.5"
|
||||
disabled={disabled}
|
||||
data-testid="model-selector"
|
||||
>
|
||||
{currentProvider === 'cursor' && (
|
||||
<span className="w-2 h-2 rounded-full bg-purple-500 mr-1" />
|
||||
)}
|
||||
{displayLabel}
|
||||
<ChevronDown className="w-3 h-3 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-72 max-h-80 overflow-y-auto">
|
||||
{/* Claude Models Section */}
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Claude</DropdownMenuLabel>
|
||||
{CLAUDE_MODELS.map((model) => (
|
||||
<DropdownMenuItem
|
||||
key={model.id}
|
||||
onClick={() => onModelSelect(model.id as ModelAlias)}
|
||||
className={cn('cursor-pointer', selectedModel === model.id && 'bg-accent')}
|
||||
data-testid={`model-option-${model.id}`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{model.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{model.description}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
{/* Cursor Models Section */}
|
||||
{filteredCursorModels.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground flex items-center gap-2">
|
||||
Cursor CLI
|
||||
{!isCursorAvailable && (
|
||||
<span className="text-amber-500 flex items-center gap-1 ml-auto">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Setup required
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
{filteredCursorModels.map((model) => (
|
||||
<DropdownMenuItem
|
||||
key={model.id}
|
||||
onClick={() => onModelSelect(model.id as CursorModelId)}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
selectedModel === model.id && 'bg-accent',
|
||||
!isCursorAvailable && 'opacity-50'
|
||||
)}
|
||||
disabled={!isCursorAvailable}
|
||||
data-testid={`model-option-${model.id}`}
|
||||
>
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{model.label}</span>
|
||||
{model.hasThinking && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-500/10 text-purple-600 dark:text-purple-400">
|
||||
Thinking
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{model.description}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<PhaseModelSelector value={value} onChange={onChange} disabled={disabled} compact align="end" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,10 +36,22 @@ import {
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
|
||||
interface PhaseModelSelectorProps {
|
||||
label: string;
|
||||
description: string;
|
||||
/** Label shown in full mode */
|
||||
label?: string;
|
||||
/** Description shown in full mode */
|
||||
description?: string;
|
||||
/** Current model selection */
|
||||
value: PhaseModelEntry;
|
||||
/** Callback when model is selected */
|
||||
onChange: (entry: PhaseModelEntry) => void;
|
||||
/** Compact mode - just shows the button trigger without label/description wrapper */
|
||||
compact?: boolean;
|
||||
/** Custom trigger class name */
|
||||
triggerClassName?: string;
|
||||
/** Popover alignment */
|
||||
align?: 'start' | 'end';
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function PhaseModelSelector({
|
||||
@@ -47,6 +59,10 @@ export function PhaseModelSelector({
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
compact = false,
|
||||
triggerClassName,
|
||||
align = 'end',
|
||||
disabled = false,
|
||||
}: PhaseModelSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [expandedGroup, setExpandedGroup] = React.useState<string | null>(null);
|
||||
@@ -505,6 +521,119 @@ export function PhaseModelSelector({
|
||||
);
|
||||
};
|
||||
|
||||
// Compact trigger button (for agent view etc.)
|
||||
const compactTrigger = (
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'h-11 gap-1 text-xs font-medium rounded-xl border-border px-2.5',
|
||||
triggerClassName
|
||||
)}
|
||||
data-testid="model-selector"
|
||||
>
|
||||
{currentModel?.icon && <currentModel.icon className="h-4 w-4 text-muted-foreground/70" />}
|
||||
<span className="truncate text-sm">
|
||||
{currentModel?.label?.replace('Claude ', '') || 'Select model...'}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
// Full trigger button (for settings view)
|
||||
const fullTrigger = (
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'w-[260px] justify-between h-9 px-3 bg-background/50 border-border/50 hover:bg-background/80 hover:text-foreground',
|
||||
triggerClassName
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{currentModel?.icon && <currentModel.icon className="h-4 w-4 text-muted-foreground/70" />}
|
||||
<span className="truncate text-sm">{currentModel?.label || 'Select model...'}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
// The popover content (shared between both modes)
|
||||
const popoverContent = (
|
||||
<PopoverContent className="w-[320px] p-0" align={align}>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search models..." />
|
||||
<CommandList ref={commandListRef} className="max-h-[300px]">
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
|
||||
{favorites.length > 0 && (
|
||||
<>
|
||||
<CommandGroup heading="Favorites">
|
||||
{(() => {
|
||||
const renderedGroups = new Set<string>();
|
||||
return favorites.map((model) => {
|
||||
// Check if this favorite is part of a grouped model
|
||||
if (model.provider === 'cursor') {
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
const group = getModelGroup(cursorId);
|
||||
if (group) {
|
||||
// Skip if we already rendered this group
|
||||
if (renderedGroups.has(group.baseId)) {
|
||||
return null;
|
||||
}
|
||||
renderedGroups.add(group.baseId);
|
||||
// Find the group in groupedModels (which has filtered variants)
|
||||
const filteredGroup = groupedModels.find((g) => g.baseId === group.baseId);
|
||||
if (filteredGroup) {
|
||||
return renderGroupedModelItem(filteredGroup);
|
||||
}
|
||||
}
|
||||
// Standalone Cursor model
|
||||
return renderCursorModelItem(model);
|
||||
}
|
||||
// Claude model
|
||||
return renderClaudeModelItem(model);
|
||||
});
|
||||
})()}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{claude.length > 0 && (
|
||||
<CommandGroup heading="Claude Models">
|
||||
{claude.map((model) => renderClaudeModelItem(model))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{(groupedModels.length > 0 || standaloneCursorModels.length > 0) && (
|
||||
<CommandGroup heading="Cursor Models">
|
||||
{/* Grouped models with secondary popover */}
|
||||
{groupedModels.map((group) => renderGroupedModelItem(group))}
|
||||
{/* Standalone models */}
|
||||
{standaloneCursorModels.map((model) => renderCursorModelItem(model))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
);
|
||||
|
||||
// Compact mode - just the popover with compact trigger
|
||||
if (compact) {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>{compactTrigger}</PopoverTrigger>
|
||||
{popoverContent}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
// Full mode - with label and description wrapper
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -521,81 +650,8 @@ export function PhaseModelSelector({
|
||||
|
||||
{/* Model Selection Popover */}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-[260px] justify-between h-9 px-3 bg-background/50 border-border/50 hover:bg-background/80 hover:text-foreground"
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{currentModel?.icon && (
|
||||
<currentModel.icon className="h-4 w-4 text-muted-foreground/70" />
|
||||
)}
|
||||
<span className="truncate text-sm">{currentModel?.label || 'Select model...'}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[320px] p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search models..." />
|
||||
<CommandList ref={commandListRef} className="max-h-[300px]">
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
|
||||
{favorites.length > 0 && (
|
||||
<>
|
||||
<CommandGroup heading="Favorites">
|
||||
{(() => {
|
||||
const renderedGroups = new Set<string>();
|
||||
return favorites.map((model) => {
|
||||
// Check if this favorite is part of a grouped model
|
||||
if (model.provider === 'cursor') {
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
const group = getModelGroup(cursorId);
|
||||
if (group) {
|
||||
// Skip if we already rendered this group
|
||||
if (renderedGroups.has(group.baseId)) {
|
||||
return null;
|
||||
}
|
||||
renderedGroups.add(group.baseId);
|
||||
// Find the group in groupedModels (which has filtered variants)
|
||||
const filteredGroup = groupedModels.find(
|
||||
(g) => g.baseId === group.baseId
|
||||
);
|
||||
if (filteredGroup) {
|
||||
return renderGroupedModelItem(filteredGroup);
|
||||
}
|
||||
}
|
||||
// Standalone Cursor model
|
||||
return renderCursorModelItem(model);
|
||||
}
|
||||
// Claude model
|
||||
return renderClaudeModelItem(model);
|
||||
});
|
||||
})()}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{claude.length > 0 && (
|
||||
<CommandGroup heading="Claude Models">
|
||||
{claude.map((model) => renderClaudeModelItem(model))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{(groupedModels.length > 0 || standaloneCursorModels.length > 0) && (
|
||||
<CommandGroup heading="Cursor Models">
|
||||
{/* Grouped models with secondary popover */}
|
||||
{groupedModels.map((group) => renderGroupedModelItem(group))}
|
||||
{/* Standalone models */}
|
||||
{standaloneCursorModels.map((model) => renderCursorModelItem(model))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
<PopoverTrigger asChild>{fullTrigger}</PopoverTrigger>
|
||||
{popoverContent}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user