feat(web): per-thread/per-spoon runtime picker and Anthropic OAuth provider profile

This commit is contained in:
Gabriel Brown
2026-07-11 11:20:33 -04:00
parent 6ec2e51312
commit 702e53a4ba
4 changed files with 190 additions and 36 deletions
@@ -25,9 +25,17 @@ import {
Textarea,
} from '@spoon/ui';
type RuntimeName = 'codex' | 'opencode' | 'claude';
const runtimeLabels: Record<RuntimeName, string> = {
codex: 'Codex',
opencode: 'OpenCode',
claude: 'Claude',
};
type AgentSettings = {
defaultBaseBranch?: string;
runtime?: 'opencode' | 'openai_direct';
runtime?: RuntimeName | 'openai_direct';
agentModel: string;
reasoningEffort: string;
envFilePath?: string;
@@ -72,6 +80,7 @@ export const ThreadWorkspaceForm = ({
const [aiProviderProfileId, setAiProviderProfileId] = useState(
agentSettings?.aiProviderProfileId ?? '__settings',
);
const [runtime, setRuntime] = useState<RuntimeName | undefined>();
const [submitting, setSubmitting] = useState(false);
const effectiveProviderProfileId =
aiProviderProfileId === '__settings'
@@ -87,6 +96,18 @@ export const ThreadWorkspaceForm = ({
(agentSettings?.aiProviderProfileId ?? defaultProfile?._id)
: profile._id === aiProviderProfileId,
);
const supportedRuntimes = (selectedProfile?.supportedRuntimes ??
[]) as RuntimeName[];
const settingsRuntime = agentSettings?.runtime as RuntimeName | undefined;
const effectiveRuntime =
runtime && supportedRuntimes.includes(runtime)
? runtime
: settingsRuntime && supportedRuntimes.includes(settingsRuntime)
? settingsRuntime
: supportedRuntimes[0];
const runtimeValid = Boolean(
effectiveRuntime && supportedRuntimes.includes(effectiveRuntime),
);
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -97,6 +118,7 @@ export const ThreadWorkspaceForm = ({
prompt,
baseBranch,
requestedBranchName: requestedBranchName || undefined,
runtime: effectiveRuntime,
materializeEnvFile,
envFilePath: materializeEnvFile ? envFilePath : undefined,
aiProviderProfileId:
@@ -140,7 +162,22 @@ export const ThreadWorkspaceForm = ({
<div className='grid gap-3 md:grid-cols-2'>
<div className='grid gap-2'>
<Label>Workspace runtime</Label>
<Input value='OpenCode workspace' disabled />
<Select
value={effectiveRuntime ?? ''}
onValueChange={(value) => setRuntime(value as RuntimeName)}
disabled={!supportedRuntimes.length}
>
<SelectTrigger>
<SelectValue placeholder='Select a provider first' />
</SelectTrigger>
<SelectContent>
{supportedRuntimes.map((name) => (
<SelectItem key={name} value={name}>
{runtimeLabels[name]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='grid gap-2'>
<Label>AI provider</Label>
@@ -219,7 +256,10 @@ export const ThreadWorkspaceForm = ({
<strong>{selectedProfile?.reasoningEffort ?? 'medium'}</strong>
</span>
</div>
<Button type='submit' disabled={submitting || !hasProvider}>
<Button
type='submit'
disabled={submitting || !hasProvider || !runtimeValid}
>
{submitting ? 'Creating...' : 'Create thread'}
</Button>
</form>