mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
opencode support
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle, Bot } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CliStatus } from '../shared/types';
|
||||
|
||||
export type OpencodeAuthMethod =
|
||||
| 'api_key_env' // ANTHROPIC_API_KEY or other provider env vars
|
||||
| 'api_key' // Manually stored API key
|
||||
| 'oauth' // OAuth authentication
|
||||
| 'config_file' // Config file with credentials
|
||||
| 'none';
|
||||
|
||||
export interface OpencodeAuthStatus {
|
||||
authenticated: boolean;
|
||||
method: OpencodeAuthMethod;
|
||||
hasApiKey?: boolean;
|
||||
hasEnvApiKey?: boolean;
|
||||
hasOAuthToken?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function getAuthMethodLabel(method: OpencodeAuthMethod): string {
|
||||
switch (method) {
|
||||
case 'api_key':
|
||||
return 'API Key';
|
||||
case 'api_key_env':
|
||||
return 'API Key (Environment)';
|
||||
case 'oauth':
|
||||
return 'OAuth Authentication';
|
||||
case 'config_file':
|
||||
return 'Configuration File';
|
||||
default:
|
||||
return method || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
interface OpencodeCliStatusProps {
|
||||
status: CliStatus | null;
|
||||
authStatus?: OpencodeAuthStatus | null;
|
||||
isChecking: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
function SkeletonPulse({ className }: { className?: string }) {
|
||||
return <div className={cn('animate-pulse bg-muted/50 rounded', className)} />;
|
||||
}
|
||||
|
||||
export function OpencodeCliStatusSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<SkeletonPulse className="w-9 h-9 rounded-xl" />
|
||||
<SkeletonPulse className="h-6 w-36" />
|
||||
</div>
|
||||
<SkeletonPulse className="w-9 h-9 rounded-lg" />
|
||||
</div>
|
||||
<div className="ml-12">
|
||||
<SkeletonPulse className="h-4 w-80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Installation status skeleton */}
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl border border-border/30 bg-muted/10">
|
||||
<SkeletonPulse className="w-10 h-10 rounded-xl" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<SkeletonPulse className="h-4 w-40" />
|
||||
<SkeletonPulse className="h-3 w-32" />
|
||||
<SkeletonPulse className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Auth status skeleton */}
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl border border-border/30 bg-muted/10">
|
||||
<SkeletonPulse className="w-10 h-10 rounded-xl" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<SkeletonPulse className="h-4 w-28" />
|
||||
<SkeletonPulse className="h-3 w-36" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpencodeModelConfigSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<SkeletonPulse className="w-9 h-9 rounded-xl" />
|
||||
<SkeletonPulse className="h-6 w-40" />
|
||||
</div>
|
||||
<div className="ml-12">
|
||||
<SkeletonPulse className="h-4 w-72" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Default Model skeleton */}
|
||||
<div className="space-y-2">
|
||||
<SkeletonPulse className="h-4 w-24" />
|
||||
<SkeletonPulse className="h-10 w-full rounded-md" />
|
||||
</div>
|
||||
{/* Available Models skeleton */}
|
||||
<div className="space-y-3">
|
||||
<SkeletonPulse className="h-4 w-32" />
|
||||
{/* Provider group skeleton */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonPulse className="w-4 h-4 rounded" />
|
||||
<SkeletonPulse className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-3 rounded-xl border border-border/30 bg-muted/10"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SkeletonPulse className="w-5 h-5 rounded" />
|
||||
<div className="space-y-1.5">
|
||||
<SkeletonPulse className="h-4 w-32" />
|
||||
<SkeletonPulse className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonPulse className="h-5 w-12 rounded-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpencodeCliStatus({
|
||||
status,
|
||||
authStatus,
|
||||
isChecking,
|
||||
onRefresh,
|
||||
}: OpencodeCliStatusProps) {
|
||||
if (!status) return <OpencodeCliStatusSkeleton />;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Bot className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">OpenCode CLI</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRefresh}
|
||||
disabled={isChecking}
|
||||
data-testid="refresh-opencode-cli"
|
||||
title="Refresh OpenCode CLI detection"
|
||||
className={cn(
|
||||
'h-9 w-9 rounded-lg',
|
||||
'hover:bg-accent/50 hover:scale-105',
|
||||
'transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={cn('w-4 h-4', isChecking && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
OpenCode CLI provides multi-provider AI support with Claude, GPT, and Gemini models.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{status.success && status.status === 'installed' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-emerald-400">OpenCode CLI Installed</p>
|
||||
<div className="text-xs text-emerald-400/70 mt-1.5 space-y-0.5">
|
||||
{status.method && (
|
||||
<p>
|
||||
Method: <span className="font-mono">{status.method}</span>
|
||||
</p>
|
||||
)}
|
||||
{status.version && (
|
||||
<p>
|
||||
Version: <span className="font-mono">{status.version}</span>
|
||||
</p>
|
||||
)}
|
||||
{status.path && (
|
||||
<p className="truncate" title={status.path}>
|
||||
Path: <span className="font-mono text-[10px]">{status.path}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authentication Status */}
|
||||
{authStatus?.authenticated ? (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-emerald-400">Authenticated</p>
|
||||
<div className="text-xs text-emerald-400/70 mt-1.5">
|
||||
<p>
|
||||
Method:{' '}
|
||||
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
|
||||
<XCircle className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||
<p className="text-xs text-amber-400/70 mt-1">
|
||||
Run{' '}
|
||||
<code className="font-mono bg-amber-500/10 px-1 rounded">opencode auth</code> or
|
||||
set an API key to authenticate.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status.recommendation && (
|
||||
<p className="text-xs text-muted-foreground/70 ml-1">{status.recommendation}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
|
||||
<AlertCircle className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-400">OpenCode CLI Not Detected</p>
|
||||
<p className="text-xs text-amber-400/70 mt-1">
|
||||
{status.recommendation || 'Install OpenCode CLI to use multi-provider AI models.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{status.installCommands && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-medium text-foreground/80">Installation Commands:</p>
|
||||
<div className="space-y-2">
|
||||
{status.installCommands.npm && (
|
||||
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
|
||||
npm
|
||||
</p>
|
||||
<code className="text-xs text-foreground/80 font-mono break-all">
|
||||
{status.installCommands.npm}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{status.installCommands.macos && (
|
||||
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
|
||||
macOS/Linux
|
||||
</p>
|
||||
<code className="text-xs text-foreground/80 font-mono break-all">
|
||||
{status.installCommands.macos}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MessageSquareText,
|
||||
User,
|
||||
Shield,
|
||||
Cpu,
|
||||
} from 'lucide-react';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||
@@ -41,6 +42,7 @@ export const GLOBAL_NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: 'claude-provider', label: 'Claude', icon: AnthropicIcon },
|
||||
{ id: 'cursor-provider', label: 'Cursor', icon: CursorIcon },
|
||||
{ id: 'codex-provider', label: 'Codex', icon: OpenAIIcon },
|
||||
{ id: 'opencode-provider', label: 'OpenCode', icon: Cpu },
|
||||
],
|
||||
},
|
||||
{ id: 'mcp-servers', label: 'MCP Servers', icon: Plug },
|
||||
|
||||
@@ -7,6 +7,7 @@ export type SettingsViewId =
|
||||
| 'claude-provider'
|
||||
| 'cursor-provider'
|
||||
| 'codex-provider'
|
||||
| 'opencode-provider'
|
||||
| 'mcp-servers'
|
||||
| 'prompts'
|
||||
| 'model-defaults'
|
||||
|
||||
@@ -8,10 +8,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Cpu } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CodexModelId } from '@automaker/types';
|
||||
import { CODEX_MODEL_MAP } from '@automaker/types';
|
||||
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
interface CodexModelConfigurationProps {
|
||||
@@ -165,18 +163,6 @@ export function CodexModelConfiguration({
|
||||
);
|
||||
}
|
||||
|
||||
function getModelDisplayName(modelId: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
'gpt-5.2-codex': 'GPT-5.2-Codex',
|
||||
'gpt-5-codex': 'GPT-5-Codex',
|
||||
'gpt-5-codex-mini': 'GPT-5-Codex-Mini',
|
||||
'codex-1': 'Codex-1',
|
||||
'codex-mini-latest': 'Codex-Mini-Latest',
|
||||
'gpt-5': 'GPT-5',
|
||||
};
|
||||
return displayNames[modelId] || modelId;
|
||||
}
|
||||
|
||||
function supportsReasoningEffort(modelId: string): boolean {
|
||||
const reasoningModels = ['gpt-5.2-codex', 'gpt-5-codex', 'gpt-5', 'codex-1'];
|
||||
return reasoningModels.includes(modelId);
|
||||
|
||||
@@ -2,3 +2,4 @@ export { ProviderTabs } from './provider-tabs';
|
||||
export { ClaudeSettingsTab } from './claude-settings-tab';
|
||||
export { CursorSettingsTab } from './cursor-settings-tab';
|
||||
export { CodexSettingsTab } from './codex-settings-tab';
|
||||
export { OpencodeSettingsTab } from './opencode-settings-tab';
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Terminal, Cloud, Cpu, Brain, Sparkles, Zap } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { OpencodeModelId, OpencodeProvider, OpencodeModelConfig } from '@automaker/types';
|
||||
import { OPENCODE_MODELS, OPENCODE_MODEL_CONFIG_MAP } from '@automaker/types';
|
||||
import { AnthropicIcon } from '@/components/ui/provider-icon';
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
interface OpencodeModelConfigurationProps {
|
||||
enabledOpencodeModels: OpencodeModelId[];
|
||||
opencodeDefaultModel: OpencodeModelId;
|
||||
isSaving: boolean;
|
||||
onDefaultModelChange: (model: OpencodeModelId) => void;
|
||||
onModelToggle: (model: OpencodeModelId, enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate icon component for a given OpenCode provider
|
||||
*/
|
||||
function getProviderIcon(provider: OpencodeProvider): ComponentType<{ className?: string }> {
|
||||
switch (provider) {
|
||||
case 'opencode':
|
||||
return Terminal;
|
||||
case 'amazon-bedrock-anthropic':
|
||||
return AnthropicIcon;
|
||||
case 'amazon-bedrock-deepseek':
|
||||
return Brain;
|
||||
case 'amazon-bedrock-amazon':
|
||||
return Cloud;
|
||||
case 'amazon-bedrock-meta':
|
||||
return Cpu;
|
||||
case 'amazon-bedrock-mistral':
|
||||
return Sparkles;
|
||||
case 'amazon-bedrock-qwen':
|
||||
return Zap;
|
||||
default:
|
||||
return Terminal;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted provider label for display
|
||||
*/
|
||||
function getProviderLabel(provider: OpencodeProvider): string {
|
||||
switch (provider) {
|
||||
case 'opencode':
|
||||
return 'OpenCode (Free)';
|
||||
case 'amazon-bedrock-anthropic':
|
||||
return 'Claude (Bedrock)';
|
||||
case 'amazon-bedrock-deepseek':
|
||||
return 'DeepSeek (Bedrock)';
|
||||
case 'amazon-bedrock-amazon':
|
||||
return 'Amazon Nova (Bedrock)';
|
||||
case 'amazon-bedrock-meta':
|
||||
return 'Meta Llama (Bedrock)';
|
||||
case 'amazon-bedrock-mistral':
|
||||
return 'Mistral (Bedrock)';
|
||||
case 'amazon-bedrock-qwen':
|
||||
return 'Qwen (Bedrock)';
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
export function OpencodeModelConfiguration({
|
||||
enabledOpencodeModels,
|
||||
opencodeDefaultModel,
|
||||
isSaving,
|
||||
onDefaultModelChange,
|
||||
onModelToggle,
|
||||
}: OpencodeModelConfigurationProps) {
|
||||
// Group models by provider for organized display
|
||||
const modelsByProvider = OPENCODE_MODELS.reduce(
|
||||
(acc, model) => {
|
||||
if (!acc[model.provider]) {
|
||||
acc[model.provider] = [];
|
||||
}
|
||||
acc[model.provider].push(model);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<OpencodeProvider, OpencodeModelConfig[]>
|
||||
);
|
||||
|
||||
// Order: Free tier first, then Claude, then others
|
||||
const providerOrder: OpencodeProvider[] = [
|
||||
'opencode',
|
||||
'amazon-bedrock-anthropic',
|
||||
'amazon-bedrock-deepseek',
|
||||
'amazon-bedrock-amazon',
|
||||
'amazon-bedrock-meta',
|
||||
'amazon-bedrock-mistral',
|
||||
'amazon-bedrock-qwen',
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Model Configuration
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure which OpenCode models are available in the feature modal
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Default Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Default Model</Label>
|
||||
<Select
|
||||
value={opencodeDefaultModel}
|
||||
onValueChange={(v) => onDefaultModelChange(v as OpencodeModelId)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{enabledOpencodeModels.map((modelId) => {
|
||||
const model = OPENCODE_MODEL_CONFIG_MAP[modelId];
|
||||
if (!model) return null;
|
||||
const ProviderIconComponent = getProviderIcon(model.provider);
|
||||
return (
|
||||
<SelectItem key={modelId} value={modelId}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ProviderIconComponent className="w-4 h-4" />
|
||||
<span>{model.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Available Models grouped by provider */}
|
||||
<div className="space-y-4">
|
||||
<Label>Available Models</Label>
|
||||
{providerOrder.map((provider) => {
|
||||
const models = modelsByProvider[provider];
|
||||
if (!models || models.length === 0) return null;
|
||||
|
||||
const ProviderIconComponent = getProviderIcon(provider);
|
||||
|
||||
return (
|
||||
<div key={provider} className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<ProviderIconComponent className="w-4 h-4" />
|
||||
<span className="font-medium">{getProviderLabel(provider)}</span>
|
||||
{provider === 'opencode' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-green-500/10 text-green-500 border-green-500/30"
|
||||
>
|
||||
Free
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{models.map((model) => {
|
||||
const isEnabled = enabledOpencodeModels.includes(model.id);
|
||||
const isDefault = model.id === opencodeDefaultModel;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex items-center justify-between p-3 rounded-xl border border-border/50 bg-card/50 hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) => onModelToggle(model.id, !!checked)}
|
||||
disabled={isSaving || isDefault}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{model.label}</span>
|
||||
{model.supportsVision && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Vision
|
||||
</Badge>
|
||||
)}
|
||||
{model.tier === 'free' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-green-500/10 text-green-500 border-green-500/30"
|
||||
>
|
||||
Free
|
||||
</Badge>
|
||||
)}
|
||||
{isDefault && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{model.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import {
|
||||
OpencodeCliStatus,
|
||||
OpencodeCliStatusSkeleton,
|
||||
OpencodeModelConfigSkeleton,
|
||||
} from '../cli-status/opencode-cli-status';
|
||||
import { OpencodeModelConfiguration } from './opencode-model-configuration';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import type { CliStatus as SharedCliStatus } from '../shared/types';
|
||||
import type { OpencodeModelId } from '@automaker/types';
|
||||
import type { OpencodeAuthStatus } from '../cli-status/opencode-cli-status';
|
||||
|
||||
const logger = createLogger('OpencodeSettings');
|
||||
|
||||
export function OpencodeSettingsTab() {
|
||||
const {
|
||||
enabledOpencodeModels,
|
||||
opencodeDefaultModel,
|
||||
setOpencodeDefaultModel,
|
||||
toggleOpencodeModel,
|
||||
} = useAppStore();
|
||||
|
||||
const [isCheckingOpencodeCli, setIsCheckingOpencodeCli] = useState(false);
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [cliStatus, setCliStatus] = useState<SharedCliStatus | null>(null);
|
||||
const [authStatus, setAuthStatus] = useState<OpencodeAuthStatus | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Load OpenCode CLI status on mount
|
||||
useEffect(() => {
|
||||
const checkOpencodeStatus = async () => {
|
||||
setIsCheckingOpencodeCli(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.setup?.getOpencodeStatus) {
|
||||
const result = await api.setup.getOpencodeStatus();
|
||||
setCliStatus({
|
||||
success: result.success,
|
||||
status: result.installed ? 'installed' : 'not_installed',
|
||||
method: result.auth?.method,
|
||||
version: result.version,
|
||||
path: result.path,
|
||||
recommendation: result.recommendation,
|
||||
installCommands: result.installCommands,
|
||||
});
|
||||
// Set auth status if available
|
||||
if (result.auth) {
|
||||
setAuthStatus({
|
||||
authenticated: result.auth.authenticated,
|
||||
method: (result.auth.method as OpencodeAuthStatus['method']) || 'none',
|
||||
hasApiKey: result.auth.hasApiKey,
|
||||
hasEnvApiKey: result.auth.hasEnvApiKey,
|
||||
hasOAuthToken: result.auth.hasOAuthToken,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback for web mode or when API is not available
|
||||
setCliStatus({
|
||||
success: false,
|
||||
status: 'not_installed',
|
||||
recommendation: 'OpenCode CLI detection is only available in desktop mode.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to check OpenCode CLI status:', error);
|
||||
setCliStatus({
|
||||
success: false,
|
||||
status: 'not_installed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsCheckingOpencodeCli(false);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
};
|
||||
checkOpencodeStatus();
|
||||
}, []);
|
||||
|
||||
const handleRefreshOpencodeCli = useCallback(async () => {
|
||||
setIsCheckingOpencodeCli(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.setup?.getOpencodeStatus) {
|
||||
const result = await api.setup.getOpencodeStatus();
|
||||
setCliStatus({
|
||||
success: result.success,
|
||||
status: result.installed ? 'installed' : 'not_installed',
|
||||
method: result.auth?.method,
|
||||
version: result.version,
|
||||
path: result.path,
|
||||
recommendation: result.recommendation,
|
||||
installCommands: result.installCommands,
|
||||
});
|
||||
// Update auth status if available
|
||||
if (result.auth) {
|
||||
setAuthStatus({
|
||||
authenticated: result.auth.authenticated,
|
||||
method: (result.auth.method as OpencodeAuthStatus['method']) || 'none',
|
||||
hasApiKey: result.auth.hasApiKey,
|
||||
hasEnvApiKey: result.auth.hasEnvApiKey,
|
||||
hasOAuthToken: result.auth.hasOAuthToken,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to refresh OpenCode CLI status:', error);
|
||||
toast.error('Failed to refresh OpenCode CLI status');
|
||||
} finally {
|
||||
setIsCheckingOpencodeCli(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDefaultModelChange = useCallback(
|
||||
(model: OpencodeModelId) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
setOpencodeDefaultModel(model);
|
||||
toast.success('Default model updated');
|
||||
} catch (error) {
|
||||
toast.error('Failed to update default model');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[setOpencodeDefaultModel]
|
||||
);
|
||||
|
||||
const handleModelToggle = useCallback(
|
||||
(model: OpencodeModelId, enabled: boolean) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
toggleOpencodeModel(model, enabled);
|
||||
} catch (error) {
|
||||
toast.error('Failed to update models');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[toggleOpencodeModel]
|
||||
);
|
||||
|
||||
// Show loading skeleton during initial load
|
||||
if (isInitialLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<OpencodeCliStatusSkeleton />
|
||||
<OpencodeModelConfigSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isCliInstalled = cliStatus?.success && cliStatus?.status === 'installed';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<OpencodeCliStatus
|
||||
status={cliStatus}
|
||||
authStatus={authStatus}
|
||||
isChecking={isCheckingOpencodeCli}
|
||||
onRefresh={handleRefreshOpencodeCli}
|
||||
/>
|
||||
|
||||
{/* Model Configuration - Only show when CLI is installed */}
|
||||
{isCliInstalled && (
|
||||
<OpencodeModelConfiguration
|
||||
enabledOpencodeModels={enabledOpencodeModels}
|
||||
opencodeDefaultModel={opencodeDefaultModel}
|
||||
isSaving={isSaving}
|
||||
onDefaultModelChange={handleDefaultModelChange}
|
||||
onModelToggle={handleModelToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OpencodeSettingsTab;
|
||||
@@ -1,18 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import { Cpu } from 'lucide-react';
|
||||
import { CursorSettingsTab } from './cursor-settings-tab';
|
||||
import { ClaudeSettingsTab } from './claude-settings-tab';
|
||||
import { CodexSettingsTab } from './codex-settings-tab';
|
||||
import { OpencodeSettingsTab } from './opencode-settings-tab';
|
||||
|
||||
interface ProviderTabsProps {
|
||||
defaultTab?: 'claude' | 'cursor' | 'codex';
|
||||
defaultTab?: 'claude' | 'cursor' | 'codex' | 'opencode';
|
||||
}
|
||||
|
||||
export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
|
||||
return (
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 mb-6">
|
||||
<TabsList className="grid w-full grid-cols-4 mb-6">
|
||||
<TabsTrigger value="claude" className="flex items-center gap-2">
|
||||
<AnthropicIcon className="w-4 h-4" />
|
||||
Claude
|
||||
@@ -25,6 +27,10 @@ export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
|
||||
<OpenAIIcon className="w-4 h-4" />
|
||||
Codex
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="opencode" className="flex items-center gap-2">
|
||||
<Cpu className="w-4 h-4" />
|
||||
OpenCode
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="claude">
|
||||
@@ -38,6 +44,10 @@ export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
|
||||
<TabsContent value="codex">
|
||||
<CodexSettingsTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="opencode">
|
||||
<OpencodeSettingsTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user