Move to threads based system.
This commit is contained in:
@@ -15,15 +15,24 @@ import {
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch,
|
||||
Textarea,
|
||||
} from '@spoon/ui';
|
||||
|
||||
import { SecretSelector } from './secret-selector';
|
||||
|
||||
type AgentSettings = {
|
||||
defaultBaseBranch?: string;
|
||||
runtime?: 'opencode' | 'openai_direct';
|
||||
agentModel: string;
|
||||
reasoningEffort: string;
|
||||
envFilePath?: string;
|
||||
customEnvFilePath?: string;
|
||||
materializeEnvFileByDefault?: boolean;
|
||||
aiProviderProfileId?: Id<'aiProviderProfiles'>;
|
||||
};
|
||||
|
||||
export const AgentRequestForm = ({
|
||||
@@ -33,11 +42,16 @@ export const AgentRequestForm = ({
|
||||
spoon: Doc<'spoons'>;
|
||||
agentSettings?: AgentSettings | null;
|
||||
}) => {
|
||||
const secrets = useQuery(api.spoonSecrets.listForSpoon, {
|
||||
spoonId: spoon._id,
|
||||
});
|
||||
const createRequest = useMutation(api.agentRequests.create);
|
||||
const createJob = useMutation(api.agentJobs.createFromRequest);
|
||||
const secrets =
|
||||
useQuery(api.spoonSecrets.listForSpoon, {
|
||||
spoonId: spoon._id,
|
||||
}) ?? [];
|
||||
const profiles =
|
||||
useQuery(api.aiProviderProfiles.listMine, {})?.filter(
|
||||
(profile) => profile.enabled && profile.configured,
|
||||
) ?? [];
|
||||
const defaultProfile = profiles.find((profile) => profile.isDefault);
|
||||
const createThread = useMutation(api.threads.createUserThread);
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [baseBranch, setBaseBranch] = useState(
|
||||
agentSettings?.defaultBaseBranch ??
|
||||
@@ -45,30 +59,52 @@ export const AgentRequestForm = ({
|
||||
spoon.upstreamDefaultBranch,
|
||||
);
|
||||
const [requestedBranchName, setRequestedBranchName] = useState('');
|
||||
const [selectedSecretIds, setSelectedSecretIds] = useState<
|
||||
Id<'spoonSecrets'>[]
|
||||
>([]);
|
||||
const [materializeEnvFile, setMaterializeEnvFile] = useState(
|
||||
agentSettings?.materializeEnvFileByDefault ?? false,
|
||||
);
|
||||
const [envFilePath, setEnvFilePath] = useState(
|
||||
agentSettings?.envFilePath === 'custom'
|
||||
? (agentSettings.customEnvFilePath ?? '.env.local')
|
||||
: (agentSettings?.envFilePath ?? '.env.local'),
|
||||
);
|
||||
const [aiProviderProfileId, setAiProviderProfileId] = useState(
|
||||
agentSettings?.aiProviderProfileId ?? '__settings',
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const effectiveProviderProfileId =
|
||||
aiProviderProfileId === '__settings'
|
||||
? (agentSettings?.aiProviderProfileId ?? defaultProfile?._id)
|
||||
: aiProviderProfileId;
|
||||
const hasProvider = Boolean(
|
||||
effectiveProviderProfileId &&
|
||||
profiles.some((profile) => profile._id === effectiveProviderProfileId),
|
||||
);
|
||||
const selectedProfile = profiles.find((profile) =>
|
||||
aiProviderProfileId === '__settings'
|
||||
? profile._id ===
|
||||
(agentSettings?.aiProviderProfileId ?? defaultProfile?._id)
|
||||
: profile._id === aiProviderProfileId,
|
||||
);
|
||||
|
||||
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const requestId = await createRequest({
|
||||
await createThread({
|
||||
spoonId: spoon._id,
|
||||
prompt,
|
||||
targetBranch: baseBranch,
|
||||
});
|
||||
await createJob({
|
||||
requestId,
|
||||
selectedSecretIds,
|
||||
baseBranch,
|
||||
requestedBranchName: requestedBranchName || undefined,
|
||||
materializeEnvFile,
|
||||
envFilePath: materializeEnvFile ? envFilePath : undefined,
|
||||
aiProviderProfileId:
|
||||
aiProviderProfileId === '__settings'
|
||||
? undefined
|
||||
: (aiProviderProfileId as Id<'aiProviderProfiles'>),
|
||||
});
|
||||
setPrompt('');
|
||||
setRequestedBranchName('');
|
||||
setSelectedSecretIds([]);
|
||||
toast.success('Agent job queued.');
|
||||
toast.success('Thread created.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Could not queue agent job.');
|
||||
@@ -99,6 +135,32 @@ export const AgentRequestForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-3 md:grid-cols-2'>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Workspace runtime</Label>
|
||||
<Input value='OpenCode workspace' disabled />
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>AI provider</Label>
|
||||
<Select
|
||||
value={aiProviderProfileId}
|
||||
onValueChange={setAiProviderProfileId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='__settings'>
|
||||
Use default
|
||||
{defaultProfile ? ` (${defaultProfile.name})` : ''}
|
||||
</SelectItem>
|
||||
{profiles.map((profile) => (
|
||||
<SelectItem key={profile._id} value={profile._id}>
|
||||
{profile.name} · {profile.provider.replaceAll('_', ' ')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='baseBranch'>Base branch</Label>
|
||||
<Input
|
||||
@@ -117,26 +179,45 @@ export const AgentRequestForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Secrets exposed to this job</Label>
|
||||
<SecretSelector
|
||||
secrets={secrets ?? []}
|
||||
selectedSecretIds={selectedSecretIds}
|
||||
onChange={setSelectedSecretIds}
|
||||
/>
|
||||
<div className='grid gap-3 md:grid-cols-[1fr_1fr]'>
|
||||
<div className='flex items-center justify-between gap-4 rounded-md border p-3'>
|
||||
<div>
|
||||
<Label>Write Spoon secrets to env file</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
All {secrets.length} Spoon secret(s) are available as process
|
||||
env. When enabled, Spoon also writes them to this file and
|
||||
refuses to commit .env files.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={materializeEnvFile}
|
||||
onCheckedChange={setMaterializeEnvFile}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='envFilePath'>Env file path</Label>
|
||||
<Input
|
||||
id='envFilePath'
|
||||
value={envFilePath}
|
||||
disabled={!materializeEnvFile}
|
||||
onChange={(event) => setEnvFilePath(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-muted/40 grid gap-1 rounded-md p-3 text-xs'>
|
||||
<span>
|
||||
Model:{' '}
|
||||
<strong>{agentSettings?.agentModel ?? 'gpt-5.1-codex'}</strong>
|
||||
<strong>
|
||||
{selectedProfile?.defaultModel ?? 'Configure an AI provider'}
|
||||
</strong>
|
||||
</span>
|
||||
<span>
|
||||
Reasoning:{' '}
|
||||
<strong>{agentSettings?.reasoningEffort ?? 'high'}</strong>
|
||||
<strong>{selectedProfile?.reasoningEffort ?? 'medium'}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<Button type='submit' disabled={submitting}>
|
||||
{submitting ? 'Queueing...' : 'Queue agent job'}
|
||||
<Button type='submit' disabled={submitting || !hasProvider}>
|
||||
{submitting ? 'Creating...' : 'Create thread'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user