Move to threads based system.
This commit is contained in:
1 parent
8ae6c4b533
commit
206b64176b
82 files changed
+6169
-1930
No files matched your search
@@ -9,12 +9,12 @@ const formatDate = (value: number) =>
|
||||
|
||||
export const SpoonActivityTimeline = ({
|
||||
syncRuns,
|
||||
reviews,
|
||||
requests,
|
||||
threads,
|
||||
jobs,
|
||||
}: {
|
||||
syncRuns: Doc<'syncRuns'>[];
|
||||
reviews: Doc<'aiReviews'>[];
|
||||
requests: Doc<'agentRequests'>[];
|
||||
threads: Doc<'threads'>[];
|
||||
jobs: Doc<'agentJobs'>[];
|
||||
}) => {
|
||||
const items = [
|
||||
...syncRuns.map((item) => ({
|
||||
@@ -24,18 +24,18 @@ export const SpoonActivityTimeline = ({
|
||||
summary: item.summary ?? item.error ?? 'Sync run recorded.',
|
||||
time: item.createdAt,
|
||||
})),
|
||||
...reviews.map((item) => ({
|
||||
...threads.map((item) => ({
|
||||
id: item._id,
|
||||
kind: 'AI review',
|
||||
kind: item.source.replaceAll('_', ' '),
|
||||
status: item.status,
|
||||
summary: item.outputSummary ?? item.inputSummary,
|
||||
summary: item.summary ?? item.title,
|
||||
time: item.createdAt,
|
||||
})),
|
||||
...requests.map((item) => ({
|
||||
...jobs.map((item) => ({
|
||||
id: item._id,
|
||||
kind: 'Agent request',
|
||||
kind: item.jobType?.replaceAll('_', ' ') ?? 'workspace job',
|
||||
status: item.status,
|
||||
summary: item.prompt,
|
||||
summary: item.summary ?? item.prompt,
|
||||
time: item.createdAt,
|
||||
})),
|
||||
].sort((a, b) => b.time - a.time);
|
||||
@@ -62,7 +62,7 @@ export const SpoonActivityTimeline = ({
|
||||
) : (
|
||||
<Card className='shadow-none'>
|
||||
<CardContent className='text-muted-foreground p-6 text-sm'>
|
||||
Refreshes, AI reviews, and queued requests will build this timeline.
|
||||
Refreshes, threads, and workspace jobs will build this timeline.
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from 'convex/react';
|
||||
import type { ProviderModelOption } from '@/lib/models-dev';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { loadModelsDevOptions } from '@/lib/models-dev';
|
||||
import { useMutation, useQuery } from 'convex/react';
|
||||
import { Bot } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Doc } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import type { Doc, Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import {
|
||||
Button,
|
||||
@@ -24,17 +26,9 @@ import {
|
||||
} from '@spoon/ui';
|
||||
|
||||
const efforts = ['minimal', 'low', 'medium', 'high', 'xhigh'] as const;
|
||||
const modelOptions = [
|
||||
{ value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
|
||||
{ value: 'gpt-5.5', label: 'GPT-5.5' },
|
||||
{ value: 'gpt-5.5-pro', label: 'GPT-5.5 Pro' },
|
||||
{ value: 'gpt-5.4', label: 'GPT-5.4' },
|
||||
{ value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' },
|
||||
] as const;
|
||||
type AgentModel = (typeof modelOptions)[number]['value'];
|
||||
|
||||
type AgentSettings = {
|
||||
enabled: boolean;
|
||||
runtime?: 'opencode' | 'openai_direct';
|
||||
defaultBaseBranch?: string;
|
||||
branchPrefix: string;
|
||||
installCommand?: string;
|
||||
@@ -42,13 +36,14 @@ type AgentSettings = {
|
||||
testCommand?: string;
|
||||
agentModel: string;
|
||||
reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||
envFilePath?: string;
|
||||
customEnvFilePath?: string;
|
||||
materializeEnvFileByDefault?: boolean;
|
||||
autoDetectCommands?: boolean;
|
||||
allowUserFileEditing?: boolean;
|
||||
aiProviderProfileId?: Id<'aiProviderProfiles'>;
|
||||
};
|
||||
|
||||
const toAgentModel = (value?: string): AgentModel =>
|
||||
modelOptions.some((option) => option.value === value)
|
||||
? (value as AgentModel)
|
||||
: 'gpt-5.1-codex';
|
||||
|
||||
export const SpoonAgentSettingsForm = ({
|
||||
spoon,
|
||||
settings,
|
||||
@@ -57,6 +52,13 @@ export const SpoonAgentSettingsForm = ({
|
||||
settings?: AgentSettings | null;
|
||||
}) => {
|
||||
const update = useMutation(api.spoonAgentSettings.update);
|
||||
const profiles = useQuery(api.aiProviderProfiles.listMine, {}) ?? [];
|
||||
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 ??
|
||||
@@ -73,29 +75,113 @@ export const SpoonAgentSettingsForm = ({
|
||||
settings?.checkCommand ?? '',
|
||||
);
|
||||
const [testCommand, setTestCommand] = useState(settings?.testCommand ?? '');
|
||||
const [agentModel, setAgentModel] = useState<AgentModel>(
|
||||
toAgentModel(settings?.agentModel),
|
||||
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 [availableModels, setAvailableModels] = useState<ProviderModelOption[]>(
|
||||
[],
|
||||
);
|
||||
const [agentModel, setAgentModel] = useState(
|
||||
settings?.aiProviderProfileId ? settings.agentModel : '',
|
||||
);
|
||||
const [reasoningEffort, setReasoningEffort] = useState<
|
||||
'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
|
||||
>(
|
||||
settings?.reasoningEffort === 'none'
|
||||
? 'minimal'
|
||||
: (settings?.reasoningEffort ?? 'high'),
|
||||
!settings?.aiProviderProfileId
|
||||
? 'medium'
|
||||
: settings.reasoningEffort === 'none'
|
||||
? 'minimal'
|
||||
: settings.reasoningEffort,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProfile?.configured) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
loadModelsDevOptions(selectedProfile.provider)
|
||||
.then((models) => {
|
||||
if (cancelled) return;
|
||||
setAvailableModels(models);
|
||||
setAgentModel((current) =>
|
||||
current && models.some((model) => model.id === current)
|
||||
? current
|
||||
: models.some((model) => model.id === selectedProfile.defaultModel)
|
||||
? selectedProfile.defaultModel
|
||||
: (models[0]?.id ?? ''),
|
||||
);
|
||||
setReasoningEffort(
|
||||
selectedProfile.reasoningEffort === 'none'
|
||||
? 'minimal'
|
||||
: selectedProfile.reasoningEffort,
|
||||
);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
if (!cancelled) setAvailableModels([]);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
selectedProfile?.configured,
|
||||
selectedProfile?.defaultModel,
|
||||
selectedProfile?.provider,
|
||||
selectedProfile?.reasoningEffort,
|
||||
]);
|
||||
const selectableModels = selectedProfile?.configured ? availableModels : [];
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await update({
|
||||
spoonId: spoon._id,
|
||||
enabled,
|
||||
runtime: 'opencode',
|
||||
defaultBaseBranch,
|
||||
branchPrefix,
|
||||
installCommand: installCommand || undefined,
|
||||
checkCommand: checkCommand || undefined,
|
||||
testCommand: testCommand || undefined,
|
||||
agentModel,
|
||||
agentModel: agentModel.trim()
|
||||
? agentModel
|
||||
: (selectableModels[0]?.id ?? 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) {
|
||||
@@ -122,6 +208,50 @@ export const SpoonAgentSettingsForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-3 md:grid-cols-2'>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Runtime</Label>
|
||||
<Input value='OpenCode workspace' disabled />
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>AI provider profile</Label>
|
||||
<Select
|
||||
value={aiProviderProfileId}
|
||||
onValueChange={(value) => {
|
||||
setAiProviderProfileId(value);
|
||||
const nextProfile = profiles.find(
|
||||
(profile) =>
|
||||
profile._id ===
|
||||
(value === '__default' ? defaultProfile?._id : value),
|
||||
);
|
||||
if (!nextProfile?.configured) setAgentModel('');
|
||||
if (nextProfile?.configured) {
|
||||
setReasoningEffort(
|
||||
nextProfile.reasoningEffort === 'none'
|
||||
? 'minimal'
|
||||
: nextProfile.reasoningEffort,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='__default'>
|
||||
Use account default
|
||||
{defaultProfile ? ` (${defaultProfile.name})` : ''}
|
||||
</SelectItem>
|
||||
{configuredProfiles.map((profile) => (
|
||||
<SelectItem key={profile._id} value={profile._id}>
|
||||
{profile.name} · {profile.provider.replaceAll('_', ' ')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
OpenCode jobs and maintenance review threads use this profile.
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='defaultBaseBranch'>Default base branch</Label>
|
||||
<Input
|
||||
@@ -142,19 +272,26 @@ export const SpoonAgentSettingsForm = ({
|
||||
<Label htmlFor='agentModel'>Model</Label>
|
||||
<Select
|
||||
value={agentModel}
|
||||
onValueChange={(value) => setAgentModel(value as AgentModel)}
|
||||
onValueChange={setAgentModel}
|
||||
disabled={!selectableModels.length}
|
||||
>
|
||||
<SelectTrigger id='agentModel'>
|
||||
<SelectValue />
|
||||
<SelectValue placeholder='Choose a configured model' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modelOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{selectableModels.map((option) => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{!selectableModels.length ? (
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Configure an enabled AI provider profile in Settings before
|
||||
choosing a model.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Reasoning effort</Label>
|
||||
@@ -215,8 +352,80 @@ export const SpoonAgentSettingsForm = ({
|
||||
Leave blank to run the detected test script when one exists.
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Env file path</Label>
|
||||
<Select value={envFilePath} onValueChange={setEnvFilePath}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='.env'>.env</SelectItem>
|
||||
<SelectItem value='.env.local'>.env.local</SelectItem>
|
||||
<SelectItem value='.env.production'>.env.production</SelectItem>
|
||||
<SelectItem value='.env.production.local'>
|
||||
.env.production.local
|
||||
</SelectItem>
|
||||
<SelectItem value='custom'>Custom path</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{envFilePath === 'custom' ? (
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='customEnvFilePath'>Custom env path</Label>
|
||||
<Input
|
||||
id='customEnvFilePath'
|
||||
value={customEnvFilePath}
|
||||
placeholder='.env.spoon'
|
||||
onChange={(event) => setCustomEnvFilePath(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className='flex items-center justify-between gap-4 rounded-md border p-3'>
|
||||
<div>
|
||||
<Label>Materialize env file by default</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Write all Spoon secrets into the chosen .env file for new
|
||||
workspaces.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={materializeEnvFileByDefault}
|
||||
onCheckedChange={setMaterializeEnvFileByDefault}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-4 rounded-md border p-3'>
|
||||
<div>
|
||||
<Label>Auto-detect commands</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Inspect package files after cloning.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoDetectCommands}
|
||||
onCheckedChange={setAutoDetectCommands}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-4 rounded-md border p-3'>
|
||||
<div>
|
||||
<Label>Allow browser file editing</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Let users edit workspace files manually.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={allowUserFileEditing}
|
||||
onCheckedChange={setAllowUserFileEditing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type='button' onClick={save}>
|
||||
<Button
|
||||
type='button'
|
||||
onClick={save}
|
||||
disabled={
|
||||
!selectedProfile?.configured ||
|
||||
!selectableModels.some((model) => model.id === agentModel)
|
||||
}
|
||||
>
|
||||
Save agent settings
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { Doc } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import { Badge, Card, CardContent, CardHeader, CardTitle } from '@spoon/ui';
|
||||
|
||||
export const SpoonAiReviewPanel = ({
|
||||
latestReview,
|
||||
reviews,
|
||||
}: {
|
||||
latestReview?: Doc<'aiReviews'> | null;
|
||||
reviews: Doc<'aiReviews'>[];
|
||||
}) => (
|
||||
<div className='space-y-4'>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>Latest compatibility review</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{latestReview ? (
|
||||
<div className='space-y-4'>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Badge>{latestReview.risk}</Badge>
|
||||
<Badge variant='outline'>{latestReview.recommendedAction}</Badge>
|
||||
{latestReview.requiresHumanReview ? (
|
||||
<Badge variant='secondary'>Human review required</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<p className='text-sm'>
|
||||
{latestReview.outputSummary ?? latestReview.inputSummary}
|
||||
</p>
|
||||
{latestReview.reasoningSummary ? (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{latestReview.reasoningSummary}
|
||||
</p>
|
||||
) : null}
|
||||
{latestReview.potentialConflicts?.length ? (
|
||||
<div>
|
||||
<p className='text-sm font-medium'>Potential conflicts</p>
|
||||
<ul className='text-muted-foreground mt-2 list-disc space-y-1 pl-5 text-sm'>
|
||||
{latestReview.potentialConflicts.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Run an AI review after a GitHub refresh to get compatibility notes.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>Review history</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-3'>
|
||||
{reviews.length ? (
|
||||
reviews.map((review) => (
|
||||
<div key={review._id} className='border-border border p-3 text-sm'>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Badge variant='outline'>{review.status}</Badge>
|
||||
<Badge variant='secondary'>{review.risk}</Badge>
|
||||
</div>
|
||||
<p className='mt-2'>
|
||||
{review.outputSummary ?? review.inputSummary}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className='text-muted-foreground text-sm'>No AI reviews yet.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import { SpoonStatusBadge } from '@/components/spoons/spoon-status-badge';
|
||||
import { useAction } from 'convex/react';
|
||||
import { makeFunctionReference } from 'convex/server';
|
||||
import { Brain, RefreshCw, RotateCw } from 'lucide-react';
|
||||
import { RefreshCw, RotateCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Doc, Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
@@ -27,12 +27,6 @@ const syncRef = makeFunctionReference<
|
||||
unknown
|
||||
>('githubSync:syncForkWithUpstream');
|
||||
|
||||
const reviewRef = makeFunctionReference<
|
||||
'action',
|
||||
{ spoonId: Id<'spoons'> },
|
||||
{ reviewId: Id<'aiReviews'>; risk: string; recommendedAction: string }
|
||||
>('aiReviewActions:reviewLatestUpstreamChanges');
|
||||
|
||||
export const SpoonDetailHeader = ({
|
||||
spoon,
|
||||
state,
|
||||
@@ -42,7 +36,6 @@ export const SpoonDetailHeader = ({
|
||||
}) => {
|
||||
const refresh = useAction(refreshRef);
|
||||
const sync = useAction(syncRef);
|
||||
const review = useAction(reviewRef);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const canSync =
|
||||
spoon.provider === 'github' &&
|
||||
@@ -110,14 +103,6 @@ export const SpoonDetailHeader = ({
|
||||
<RefreshCw className='size-4' />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => run('AI review', () => review({ spoonId: spoon._id }))}
|
||||
disabled={Boolean(busy)}
|
||||
>
|
||||
<Brain className='size-4' />
|
||||
Review with AI
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => run('Sync', () => sync({ spoonId: spoon._id }))}
|
||||
disabled={Boolean(busy) || !canSync}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
Clock,
|
||||
GitCommit,
|
||||
GitPullRequest,
|
||||
ShieldCheck,
|
||||
MessagesSquare,
|
||||
TrendingUp,
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -17,11 +17,11 @@ const formatDate = (value?: number) =>
|
||||
export const SpoonMetrics = ({
|
||||
spoon,
|
||||
state,
|
||||
latestReview,
|
||||
latestThread,
|
||||
}: {
|
||||
spoon: Doc<'spoons'>;
|
||||
state?: Doc<'spoonRepositoryStates'> | null;
|
||||
latestReview?: Doc<'aiReviews'> | null;
|
||||
latestThread?: Doc<'threads'> | null;
|
||||
}) => {
|
||||
const metrics = [
|
||||
{
|
||||
@@ -42,9 +42,9 @@ export const SpoonMetrics = ({
|
||||
icon: GitPullRequest,
|
||||
},
|
||||
{
|
||||
label: 'Latest AI risk',
|
||||
value: latestReview?.risk ?? 'unknown',
|
||||
icon: ShieldCheck,
|
||||
label: 'Latest thread',
|
||||
value: latestThread?.status.replaceAll('_', ' ') ?? 'none',
|
||||
icon: MessagesSquare,
|
||||
},
|
||||
{
|
||||
label: 'Last check',
|
||||
|
||||
@@ -93,7 +93,7 @@ export const SpoonSettingsForm = ({
|
||||
onChange: setAutoRefreshEnabled,
|
||||
},
|
||||
{
|
||||
label: 'Auto AI review',
|
||||
label: 'Auto maintenance threads',
|
||||
value: autoReviewEnabled,
|
||||
onChange: setAutoReviewEnabled,
|
||||
},
|
||||
@@ -103,7 +103,7 @@ export const SpoonSettingsForm = ({
|
||||
onChange: setAutoSyncEnabled,
|
||||
},
|
||||
{
|
||||
label: 'Require low AI risk for sync',
|
||||
label: 'Require low-risk thread decision for sync',
|
||||
value: requireAiLowRiskForSync,
|
||||
onChange: setRequireAiLowRiskForSync,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user