'use client'; import { useState } from 'react'; import { useMutation, useQuery } from 'convex/react'; import { Bot } from 'lucide-react'; import { toast } from 'sonner'; import type { Doc, Id } from '@spoon/backend/convex/_generated/dataModel.js'; import { api } from '@spoon/backend/convex/_generated/api.js'; import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@spoon/ui'; const efforts = ['minimal', 'low', 'medium', 'high', 'xhigh'] as const; type AgentSettings = { enabled: boolean; runtime?: 'opencode' | 'openai_direct'; defaultBaseBranch?: string; branchPrefix: string; installCommand?: string; checkCommand?: string; testCommand?: string; agentModel: string; reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'; envFilePath?: string; customEnvFilePath?: string; materializeEnvFileByDefault?: boolean; autoDetectCommands?: boolean; allowUserFileEditing?: boolean; aiProviderProfileId?: Id<'aiProviderProfiles'>; }; export const SpoonAgentSettingsForm = ({ spoon, settings, }: { spoon: Doc<'spoons'>; settings?: AgentSettings | null; }) => { const update = useMutation(api.spoonAgentSettings.update); const profiles = useQuery(api.aiProviderProfiles.listMine, {}) ?? []; const modelCatalog = useQuery(api.aiProviderModels.listAvailableForUser); const configuredProfiles = profiles.filter( (profile) => profile.enabled && profile.configured, ); const defaultProfile = configuredProfiles.find( (profile) => profile.isDefault, ); const [enabled, setEnabled] = useState(settings?.enabled ?? true); const [defaultBaseBranch, setDefaultBaseBranch] = useState( settings?.defaultBaseBranch ?? spoon.forkDefaultBranch ?? spoon.upstreamDefaultBranch, ); const [branchPrefix, setBranchPrefix] = useState( settings?.branchPrefix ?? 'spoon/agent', ); const [installCommand, setInstallCommand] = useState( settings?.installCommand ?? '', ); const [checkCommand, setCheckCommand] = useState( settings?.checkCommand ?? '', ); const [testCommand, setTestCommand] = useState(settings?.testCommand ?? ''); const [envFilePath, setEnvFilePath] = useState( settings?.envFilePath ?? '.env.local', ); const [customEnvFilePath, setCustomEnvFilePath] = useState( settings?.customEnvFilePath ?? '', ); const [materializeEnvFileByDefault, setMaterializeEnvFileByDefault] = useState(settings?.materializeEnvFileByDefault ?? false); const [autoDetectCommands, setAutoDetectCommands] = useState( settings?.autoDetectCommands ?? true, ); const [allowUserFileEditing, setAllowUserFileEditing] = useState( settings?.allowUserFileEditing ?? true, ); const [aiProviderProfileId, setAiProviderProfileId] = useState( settings?.aiProviderProfileId ?? '__default', ); const selectedProfile = profiles.find( (profile) => profile._id === (aiProviderProfileId === '__default' ? defaultProfile?._id : aiProviderProfileId), ); const selectedModelProfile = modelCatalog?.profiles.find( (profile) => profile.profileId === (aiProviderProfileId === '__default' ? defaultProfile?._id : aiProviderProfileId), ); const [agentModel, setAgentModel] = useState( settings?.aiProviderProfileId ? settings.agentModel : '', ); const [reasoningEffort, setReasoningEffort] = useState< 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' >( !settings?.aiProviderProfileId ? 'medium' : settings.reasoningEffort === 'none' ? 'minimal' : settings.reasoningEffort, ); const selectableModels = selectedModelProfile?.configured ? selectedModelProfile.models : []; const selectedAgentModel = agentModel && selectableModels.some((model) => model.id === agentModel) ? agentModel : selectableModels.some( (model) => model.id === selectedModelProfile?.defaultModel, ) ? (selectedModelProfile?.defaultModel ?? '') : (selectableModels[0]?.id ?? ''); const save = async () => { try { await update({ spoonId: spoon._id, enabled, runtime: 'opencode', defaultBaseBranch, branchPrefix, installCommand: installCommand || undefined, checkCommand: checkCommand || undefined, testCommand: testCommand || undefined, agentModel: selectedAgentModel || undefined, reasoningEffort, envFilePath: envFilePath as | '.env' | '.env.local' | '.env.production' | '.env.production.local' | 'custom', customEnvFilePath: customEnvFilePath || undefined, materializeEnvFileByDefault, autoDetectCommands, allowUserFileEditing, aiProviderProfileId: aiProviderProfileId === '__default' ? undefined : (aiProviderProfileId as Id<'aiProviderProfiles'>), clearAiProviderProfile: aiProviderProfileId === '__default', }); toast.success('Agent settings saved.'); } catch (error) { console.error(error); toast.error('Could not save agent settings.'); } }; return ( Agent runtime

Workspaces use this profile. Use default resolves to your account default provider.

setDefaultBaseBranch(event.target.value)} />
setBranchPrefix(event.target.value)} />
{!selectableModels.length ? (

Configure an enabled AI provider profile with saved model options in Settings before choosing a model.

) : null}
setInstallCommand(event.target.value)} />

Leave blank to inspect the repository and choose bun, pnpm, yarn, or npm.

setCheckCommand(event.target.value)} />

Leave blank to read package.json scripts after cloning.

setTestCommand(event.target.value)} />

Leave blank to run the detected test script when one exists.

{envFilePath === 'custom' ? (
setCustomEnvFilePath(event.target.value)} />
) : null}

Write all Spoon secrets into the chosen .env file for new workspaces.

Inspect package files after cloning.

Let users edit workspace files manually.

); };