feat(web): per-thread/per-spoon runtime picker and Anthropic OAuth provider profile
This commit is contained in:
@@ -42,7 +42,11 @@ type Provider =
|
|||||||
| 'cloudflare_ai_gateway'
|
| 'cloudflare_ai_gateway'
|
||||||
| 'custom_openai_compatible'
|
| 'custom_openai_compatible'
|
||||||
| 'opencode_openai_login';
|
| 'opencode_openai_login';
|
||||||
type AuthType = 'api_key' | 'opencode_auth_json' | 'none';
|
type AuthType =
|
||||||
|
| 'api_key'
|
||||||
|
| 'opencode_auth_json'
|
||||||
|
| 'anthropic_oauth_json'
|
||||||
|
| 'none';
|
||||||
type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||||
|
|
||||||
const saveProfileRef = makeFunctionReference<
|
const saveProfileRef = makeFunctionReference<
|
||||||
@@ -69,33 +73,75 @@ const setDefaultProfileRef = makeFunctionReference<
|
|||||||
>('aiProviderProfiles:setDefault');
|
>('aiProviderProfiles:setDefault');
|
||||||
|
|
||||||
const providerOptions: {
|
const providerOptions: {
|
||||||
|
key: string;
|
||||||
value: Provider;
|
value: Provider;
|
||||||
label: string;
|
label: string;
|
||||||
authType: AuthType;
|
authType: AuthType;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ value: 'openai', label: 'OpenAI API key', authType: 'api_key' },
|
|
||||||
{ value: 'anthropic', label: 'Anthropic API key', authType: 'api_key' },
|
|
||||||
{ value: 'google', label: 'Google Gemini API key', authType: 'api_key' },
|
|
||||||
{ value: 'openrouter', label: 'OpenRouter', authType: 'api_key' },
|
|
||||||
{ value: 'requesty', label: 'Requesty', authType: 'api_key' },
|
|
||||||
{ value: 'litellm', label: 'LiteLLM', authType: 'api_key' },
|
|
||||||
{
|
{
|
||||||
|
key: 'openai',
|
||||||
|
value: 'openai',
|
||||||
|
label: 'OpenAI API key',
|
||||||
|
authType: 'api_key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'anthropic',
|
||||||
|
value: 'anthropic',
|
||||||
|
label: 'Anthropic API key',
|
||||||
|
authType: 'api_key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'google',
|
||||||
|
value: 'google',
|
||||||
|
label: 'Google Gemini API key',
|
||||||
|
authType: 'api_key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'openrouter',
|
||||||
|
value: 'openrouter',
|
||||||
|
label: 'OpenRouter',
|
||||||
|
authType: 'api_key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'requesty',
|
||||||
|
value: 'requesty',
|
||||||
|
label: 'Requesty',
|
||||||
|
authType: 'api_key',
|
||||||
|
},
|
||||||
|
{ key: 'litellm', value: 'litellm', label: 'LiteLLM', authType: 'api_key' },
|
||||||
|
{
|
||||||
|
key: 'cloudflare_ai_gateway',
|
||||||
value: 'cloudflare_ai_gateway',
|
value: 'cloudflare_ai_gateway',
|
||||||
label: 'Cloudflare AI Gateway',
|
label: 'Cloudflare AI Gateway',
|
||||||
authType: 'api_key',
|
authType: 'api_key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'custom_openai_compatible',
|
||||||
value: 'custom_openai_compatible',
|
value: 'custom_openai_compatible',
|
||||||
label: 'Custom OpenAI-compatible',
|
label: 'Custom OpenAI-compatible',
|
||||||
authType: 'api_key',
|
authType: 'api_key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'opencode_openai_login',
|
||||||
value: 'opencode_openai_login',
|
value: 'opencode_openai_login',
|
||||||
label: 'Codex ChatGPT login',
|
label: 'Codex ChatGPT login',
|
||||||
authType: 'opencode_auth_json',
|
authType: 'opencode_auth_json',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'anthropic_oauth_json',
|
||||||
|
value: 'anthropic',
|
||||||
|
label: 'Anthropic OAuth (paste credentials)',
|
||||||
|
authType: 'anthropic_oauth_json',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const defaultProviderOption: (typeof providerOptions)[number] = {
|
||||||
|
key: 'openai',
|
||||||
|
value: 'openai',
|
||||||
|
label: 'OpenAI API key',
|
||||||
|
authType: 'api_key',
|
||||||
|
};
|
||||||
|
|
||||||
const reasoningOptions: ReasoningEffort[] = [
|
const reasoningOptions: ReasoningEffort[] = [
|
||||||
'none',
|
'none',
|
||||||
'minimal',
|
'minimal',
|
||||||
@@ -112,17 +158,14 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
const removeProfile = useMutation(api.aiProviderProfiles.remove);
|
const removeProfile = useMutation(api.aiProviderProfiles.remove);
|
||||||
const [profileId, setProfileId] = useState<Id<'aiProviderProfiles'>>();
|
const [profileId, setProfileId] = useState<Id<'aiProviderProfiles'>>();
|
||||||
const [name, setName] = useState('OpenAI');
|
const [name, setName] = useState('OpenAI');
|
||||||
const [provider, setProvider] = useState<Provider>('openai');
|
const [providerKey, setProviderKey] = useState('openai');
|
||||||
const selectedProvider = useMemo(
|
const selectedProvider = useMemo(
|
||||||
() =>
|
() =>
|
||||||
providerOptions.find((option) => option.value === provider) ??
|
providerOptions.find((option) => option.key === providerKey) ??
|
||||||
({
|
defaultProviderOption,
|
||||||
value: 'openai',
|
[providerKey],
|
||||||
label: 'OpenAI API key',
|
|
||||||
authType: 'api_key',
|
|
||||||
} satisfies (typeof providerOptions)[number]),
|
|
||||||
[provider],
|
|
||||||
);
|
);
|
||||||
|
const provider = selectedProvider.value;
|
||||||
const [secret, setSecret] = useState('');
|
const [secret, setSecret] = useState('');
|
||||||
const [baseUrl, setBaseUrl] = useState('');
|
const [baseUrl, setBaseUrl] = useState('');
|
||||||
const [defaultModelValue, setDefaultModelValue] = useState(
|
const [defaultModelValue, setDefaultModelValue] = useState(
|
||||||
@@ -146,7 +189,7 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setProfileId(undefined);
|
setProfileId(undefined);
|
||||||
setProvider('openai');
|
setProviderKey('openai');
|
||||||
setSecret('');
|
setSecret('');
|
||||||
setBaseUrl('');
|
setBaseUrl('');
|
||||||
setDefaultModelValue('');
|
setDefaultModelValue('');
|
||||||
@@ -160,7 +203,16 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
const edit = (profile: (typeof profiles)[number]) => {
|
const edit = (profile: (typeof profiles)[number]) => {
|
||||||
setProfileId(profile._id);
|
setProfileId(profile._id);
|
||||||
setName(profile.name);
|
setName(profile.name);
|
||||||
setProvider(profile.provider as Provider);
|
setProviderKey(
|
||||||
|
providerOptions.find(
|
||||||
|
(option) =>
|
||||||
|
option.value === profile.provider &&
|
||||||
|
option.authType === profile.authType,
|
||||||
|
)?.key ??
|
||||||
|
providerOptions.find((option) => option.value === profile.provider)
|
||||||
|
?.key ??
|
||||||
|
'openai',
|
||||||
|
);
|
||||||
setSecret('');
|
setSecret('');
|
||||||
setBaseUrl(profile.baseUrl ?? '');
|
setBaseUrl(profile.baseUrl ?? '');
|
||||||
setDefaultModelValue(profile.defaultModel);
|
setDefaultModelValue(profile.defaultModel);
|
||||||
@@ -314,16 +366,14 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label>Provider</Label>
|
<Label>Provider</Label>
|
||||||
<Select
|
<Select
|
||||||
value={provider}
|
value={providerKey}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const nextProvider = value as Provider;
|
const nextOption =
|
||||||
setProvider(nextProvider);
|
providerOptions.find((option) => option.key === value) ??
|
||||||
resetModelOptions(nextProvider);
|
defaultProviderOption;
|
||||||
setName(
|
setProviderKey(nextOption.key);
|
||||||
providerOptions
|
resetModelOptions(nextOption.value);
|
||||||
.find((option) => option.value === nextProvider)
|
setName(nextOption.label.replace(' API key', ''));
|
||||||
?.label.replace(' API key', '') ?? 'AI provider',
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -331,7 +381,7 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{providerOptions.map((option) => (
|
{providerOptions.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.key} value={option.key}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -342,6 +392,8 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
<Label>
|
<Label>
|
||||||
{selectedProvider.authType === 'opencode_auth_json'
|
{selectedProvider.authType === 'opencode_auth_json'
|
||||||
? 'Codex auth JSON'
|
? 'Codex auth JSON'
|
||||||
|
: selectedProvider.authType === 'anthropic_oauth_json'
|
||||||
|
? 'Anthropic credentials JSON'
|
||||||
: 'API key'}
|
: 'API key'}
|
||||||
</Label>
|
</Label>
|
||||||
{selectedProvider.authType === 'opencode_auth_json' ? (
|
{selectedProvider.authType === 'opencode_auth_json' ? (
|
||||||
@@ -361,6 +413,23 @@ export const AiProviderProfilesPanel = () => {
|
|||||||
be treated like a password.
|
be treated like a password.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
|
) : selectedProvider.authType === 'anthropic_oauth_json' ? (
|
||||||
|
<>
|
||||||
|
<Textarea
|
||||||
|
value={secret}
|
||||||
|
placeholder='Paste the full .credentials.json contents.'
|
||||||
|
onChange={(event) => setSecret(event.target.value)}
|
||||||
|
/>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
Copy your Claude credentials file from{' '}
|
||||||
|
<code className='bg-muted rounded px-1 py-0.5'>
|
||||||
|
~/.claude/.credentials.json
|
||||||
|
</code>
|
||||||
|
. Spoon writes it into the isolated job workspace as
|
||||||
|
Claude's auth cache. It is stored encrypted and should
|
||||||
|
be treated like a password.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
type='password'
|
type='password'
|
||||||
|
|||||||
@@ -24,9 +24,17 @@ import {
|
|||||||
} from '@spoon/ui';
|
} from '@spoon/ui';
|
||||||
|
|
||||||
const efforts = ['minimal', 'low', 'medium', 'high', 'xhigh'] as const;
|
const efforts = ['minimal', 'low', 'medium', 'high', 'xhigh'] as const;
|
||||||
|
type RuntimeName = 'codex' | 'opencode' | 'claude';
|
||||||
|
|
||||||
|
const runtimeLabels: Record<RuntimeName, string> = {
|
||||||
|
codex: 'Codex',
|
||||||
|
opencode: 'OpenCode',
|
||||||
|
claude: 'Claude',
|
||||||
|
};
|
||||||
|
|
||||||
type AgentSettings = {
|
type AgentSettings = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
runtime?: 'opencode' | 'openai_direct';
|
runtime?: RuntimeName | 'openai_direct';
|
||||||
defaultBaseBranch?: string;
|
defaultBaseBranch?: string;
|
||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
installCommand?: string;
|
installCommand?: string;
|
||||||
@@ -91,6 +99,7 @@ export const SpoonAgentSettingsForm = ({
|
|||||||
const [aiProviderProfileId, setAiProviderProfileId] = useState(
|
const [aiProviderProfileId, setAiProviderProfileId] = useState(
|
||||||
settings?.aiProviderProfileId ?? '__default',
|
settings?.aiProviderProfileId ?? '__default',
|
||||||
);
|
);
|
||||||
|
const [runtime, setRuntime] = useState<RuntimeName | undefined>();
|
||||||
const selectedProfile = profiles.find(
|
const selectedProfile = profiles.find(
|
||||||
(profile) =>
|
(profile) =>
|
||||||
profile._id ===
|
profile._id ===
|
||||||
@@ -118,6 +127,19 @@ export const SpoonAgentSettingsForm = ({
|
|||||||
: settings.reasoningEffort,
|
: settings.reasoningEffort,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const supportedRuntimes = (selectedProfile?.supportedRuntimes ??
|
||||||
|
[]) as RuntimeName[];
|
||||||
|
const settingsRuntime = settings?.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 selectableModels = selectedModelProfile?.configured
|
const selectableModels = selectedModelProfile?.configured
|
||||||
? selectedModelProfile.models
|
? selectedModelProfile.models
|
||||||
: [];
|
: [];
|
||||||
@@ -135,7 +157,7 @@ export const SpoonAgentSettingsForm = ({
|
|||||||
await update({
|
await update({
|
||||||
spoonId: spoon._id,
|
spoonId: spoon._id,
|
||||||
enabled,
|
enabled,
|
||||||
runtime: 'opencode',
|
runtime: effectiveRuntime,
|
||||||
defaultBaseBranch,
|
defaultBaseBranch,
|
||||||
branchPrefix,
|
branchPrefix,
|
||||||
installCommand: installCommand || undefined,
|
installCommand: installCommand || undefined,
|
||||||
@@ -186,7 +208,25 @@ export const SpoonAgentSettingsForm = ({
|
|||||||
<div className='grid gap-3 md:grid-cols-2'>
|
<div className='grid gap-3 md:grid-cols-2'>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label>Runtime</Label>
|
<Label>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 profile first' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{supportedRuntimes.map((name) => (
|
||||||
|
<SelectItem key={name} value={name}>
|
||||||
|
{runtimeLabels[name]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
Runtimes available for the selected provider profile.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label>AI provider profile</Label>
|
<Label>AI provider profile</Label>
|
||||||
@@ -400,6 +440,7 @@ export const SpoonAgentSettingsForm = ({
|
|||||||
onClick={save}
|
onClick={save}
|
||||||
disabled={
|
disabled={
|
||||||
!selectedProfile?.configured ||
|
!selectedProfile?.configured ||
|
||||||
|
!runtimeValid ||
|
||||||
!selectableModels.some((model) => model.id === selectedAgentModel)
|
!selectableModels.some((model) => model.id === selectedAgentModel)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,9 +25,17 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from '@spoon/ui';
|
} from '@spoon/ui';
|
||||||
|
|
||||||
|
type RuntimeName = 'codex' | 'opencode' | 'claude';
|
||||||
|
|
||||||
|
const runtimeLabels: Record<RuntimeName, string> = {
|
||||||
|
codex: 'Codex',
|
||||||
|
opencode: 'OpenCode',
|
||||||
|
claude: 'Claude',
|
||||||
|
};
|
||||||
|
|
||||||
type AgentSettings = {
|
type AgentSettings = {
|
||||||
defaultBaseBranch?: string;
|
defaultBaseBranch?: string;
|
||||||
runtime?: 'opencode' | 'openai_direct';
|
runtime?: RuntimeName | 'openai_direct';
|
||||||
agentModel: string;
|
agentModel: string;
|
||||||
reasoningEffort: string;
|
reasoningEffort: string;
|
||||||
envFilePath?: string;
|
envFilePath?: string;
|
||||||
@@ -72,6 +80,7 @@ export const ThreadWorkspaceForm = ({
|
|||||||
const [aiProviderProfileId, setAiProviderProfileId] = useState(
|
const [aiProviderProfileId, setAiProviderProfileId] = useState(
|
||||||
agentSettings?.aiProviderProfileId ?? '__settings',
|
agentSettings?.aiProviderProfileId ?? '__settings',
|
||||||
);
|
);
|
||||||
|
const [runtime, setRuntime] = useState<RuntimeName | undefined>();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const effectiveProviderProfileId =
|
const effectiveProviderProfileId =
|
||||||
aiProviderProfileId === '__settings'
|
aiProviderProfileId === '__settings'
|
||||||
@@ -87,6 +96,18 @@ export const ThreadWorkspaceForm = ({
|
|||||||
(agentSettings?.aiProviderProfileId ?? defaultProfile?._id)
|
(agentSettings?.aiProviderProfileId ?? defaultProfile?._id)
|
||||||
: profile._id === aiProviderProfileId,
|
: 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>) => {
|
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -97,6 +118,7 @@ export const ThreadWorkspaceForm = ({
|
|||||||
prompt,
|
prompt,
|
||||||
baseBranch,
|
baseBranch,
|
||||||
requestedBranchName: requestedBranchName || undefined,
|
requestedBranchName: requestedBranchName || undefined,
|
||||||
|
runtime: effectiveRuntime,
|
||||||
materializeEnvFile,
|
materializeEnvFile,
|
||||||
envFilePath: materializeEnvFile ? envFilePath : undefined,
|
envFilePath: materializeEnvFile ? envFilePath : undefined,
|
||||||
aiProviderProfileId:
|
aiProviderProfileId:
|
||||||
@@ -140,7 +162,22 @@ export const ThreadWorkspaceForm = ({
|
|||||||
<div className='grid gap-3 md:grid-cols-2'>
|
<div className='grid gap-3 md:grid-cols-2'>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label>Workspace runtime</Label>
|
<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>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label>AI provider</Label>
|
<Label>AI provider</Label>
|
||||||
@@ -219,7 +256,10 @@ export const ThreadWorkspaceForm = ({
|
|||||||
<strong>{selectedProfile?.reasoningEffort ?? 'medium'}</strong>
|
<strong>{selectedProfile?.reasoningEffort ?? 'medium'}</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button type='submit' disabled={submitting || !hasProvider}>
|
<Button
|
||||||
|
type='submit'
|
||||||
|
disabled={submitting || !hasProvider || !runtimeValid}
|
||||||
|
>
|
||||||
{submitting ? 'Creating...' : 'Create thread'}
|
{submitting ? 'Creating...' : 'Create thread'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ const reasoningEffort = v.union(
|
|||||||
v.literal('xhigh'),
|
v.literal('xhigh'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const runtime = v.literal('opencode');
|
const runtime = v.union(
|
||||||
|
v.literal('codex'),
|
||||||
|
v.literal('opencode'),
|
||||||
|
v.literal('claude'),
|
||||||
|
);
|
||||||
|
|
||||||
const envFilePath = v.union(
|
const envFilePath = v.union(
|
||||||
v.literal('.env'),
|
v.literal('.env'),
|
||||||
@@ -123,7 +127,7 @@ export const update = mutation({
|
|||||||
patch.testCommand = optionalText(args.testCommand);
|
patch.testCommand = optionalText(args.testCommand);
|
||||||
}
|
}
|
||||||
if (args.runtime !== undefined) {
|
if (args.runtime !== undefined) {
|
||||||
patch.runtime = 'opencode';
|
patch.runtime = args.runtime;
|
||||||
}
|
}
|
||||||
if (args.agentModel !== undefined) {
|
if (args.agentModel !== undefined) {
|
||||||
patch.agentModel = optionalText(args.agentModel) ?? defaults.agentModel;
|
patch.agentModel = optionalText(args.agentModel) ?? defaults.agentModel;
|
||||||
|
|||||||
Reference in New Issue
Block a user