feat: enhance feature dialogs with OpenCode model support

- Added OpenCode model selection to AddFeatureDialog and EditFeatureDialog.
- Introduced ProfileTypeahead component for improved profile selection.
- Updated model constants to include OpenCode models and integrated them into the PhaseModelSelector.
- Enhanced planning mode options with new UI elements for OpenCode.
- Refactored existing components to streamline model handling and improve user experience.

This commit expands the functionality of the feature dialogs, allowing users to select and manage OpenCode models effectively.
This commit is contained in:
webdevcody
2026-01-09 09:02:30 -05:00
parent be88a07329
commit 41b4869068
9 changed files with 1285 additions and 809 deletions

View File

@@ -5,6 +5,7 @@ import type {
ModelAlias,
CursorModelId,
CodexModelId,
OpencodeModelId,
GroupedModel,
PhaseModelEntry,
ThinkingLevel,
@@ -23,13 +24,14 @@ import {
CLAUDE_MODELS,
CURSOR_MODELS,
CODEX_MODELS,
OPENCODE_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS,
REASONING_EFFORT_LABELS,
} from '@/components/views/board-view/shared/model-constants';
import { Check, ChevronsUpDown, Star, ChevronRight } from 'lucide-react';
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
import { Button } from '@/components/ui/button';
import {
Command,
@@ -199,6 +201,10 @@ export function PhaseModelSelector({
const codexModel = CODEX_MODELS.find((m) => m.id === selectedModel);
if (codexModel) return { ...codexModel, icon: OpenAIIcon };
// Check OpenCode models
const opencodeModel = OPENCODE_MODELS.find((m) => m.id === selectedModel);
if (opencodeModel) return { ...opencodeModel, icon: OpenCodeIcon };
return null;
}, [selectedModel, selectedThinkingLevel, availableCursorModels]);
@@ -236,11 +242,12 @@ export function PhaseModelSelector({
}, [availableCursorModels, enabledCursorModels]);
// Group models
const { favorites, claude, cursor, codex } = React.useMemo(() => {
const { favorites, claude, cursor, codex, opencode } = React.useMemo(() => {
const favs: typeof CLAUDE_MODELS = [];
const cModels: typeof CLAUDE_MODELS = [];
const curModels: typeof CURSOR_MODELS = [];
const codModels: typeof CODEX_MODELS = [];
const ocModels: typeof OPENCODE_MODELS = [];
// Process Claude Models
CLAUDE_MODELS.forEach((model) => {
@@ -269,7 +276,22 @@ export function PhaseModelSelector({
}
});
return { favorites: favs, claude: cModels, cursor: curModels, codex: codModels };
// Process OpenCode Models
OPENCODE_MODELS.forEach((model) => {
if (favoriteModels.includes(model.id)) {
favs.push(model);
} else {
ocModels.push(model);
}
});
return {
favorites: favs,
claude: cModels,
cursor: curModels,
codex: codModels,
opencode: ocModels,
};
}, [favoriteModels, availableCursorModels]);
// Render Codex model item with secondary popover for reasoning effort (only for models that support it)
@@ -453,6 +475,64 @@ export function PhaseModelSelector({
);
};
// Render OpenCode model item (simple selector, no thinking/reasoning options)
const renderOpencodeModelItem = (model: (typeof OPENCODE_MODELS)[0]) => {
const isSelected = selectedModel === model.id;
const isFavorite = favoriteModels.includes(model.id);
return (
<CommandItem
key={model.id}
value={model.label}
onSelect={() => {
onChange({ model: model.id as OpencodeModelId });
setOpen(false);
}}
className="group flex items-center justify-between py-2"
>
<div className="flex items-center gap-3 overflow-hidden">
<OpenCodeIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
)}
/>
<div className="flex flex-col truncate">
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
{model.label}
</span>
<span className="truncate text-xs text-muted-foreground">{model.description}</span>
</div>
</div>
<div className="flex items-center gap-1 ml-2">
{model.badge && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground mr-1">
{model.badge}
</span>
)}
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
isFavorite
? 'text-yellow-500 opacity-100'
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
)}
onClick={(e) => {
e.stopPropagation();
toggleFavoriteModel(model.id);
}}
>
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
</Button>
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
</div>
</CommandItem>
);
};
// Render Cursor model item (no thinking level needed)
const renderCursorModelItem = (model: (typeof CURSOR_MODELS)[0]) => {
const modelValue = stripProviderPrefix(model.id);
@@ -835,6 +915,10 @@ export function PhaseModelSelector({
if (model.provider === 'codex') {
return renderCodexModelItem(model);
}
// OpenCode model
if (model.provider === 'opencode') {
return renderOpencodeModelItem(model);
}
// Claude model
return renderClaudeModelItem(model);
});
@@ -864,6 +948,12 @@ export function PhaseModelSelector({
{codex.map((model) => renderCodexModelItem(model))}
</CommandGroup>
)}
{opencode.length > 0 && (
<CommandGroup heading="OpenCode Models">
{opencode.map((model) => renderOpencodeModelItem(model))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>