Update stuff
This commit is contained in:
1 parent
2e13febfc7
commit
4114d5595c
20 files changed
+672
-354
No files matched your search
@@ -24,6 +24,14 @@ 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;
|
||||
@@ -36,6 +44,11 @@ type AgentSettings = {
|
||||
reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||
};
|
||||
|
||||
const toAgentModel = (value?: string): AgentModel =>
|
||||
modelOptions.some((option) => option.value === value)
|
||||
? (value as AgentModel)
|
||||
: 'gpt-5.1-codex';
|
||||
|
||||
export const SpoonAgentSettingsForm = ({
|
||||
spoon,
|
||||
settings,
|
||||
@@ -60,8 +73,8 @@ export const SpoonAgentSettingsForm = ({
|
||||
settings?.checkCommand ?? '',
|
||||
);
|
||||
const [testCommand, setTestCommand] = useState(settings?.testCommand ?? '');
|
||||
const [agentModel, setAgentModel] = useState(
|
||||
settings?.agentModel ?? 'gpt-5.1-codex',
|
||||
const [agentModel, setAgentModel] = useState<AgentModel>(
|
||||
toAgentModel(settings?.agentModel),
|
||||
);
|
||||
const [reasoningEffort, setReasoningEffort] = useState<
|
||||
'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
|
||||
@@ -127,11 +140,21 @@ export const SpoonAgentSettingsForm = ({
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='agentModel'>Model</Label>
|
||||
<Input
|
||||
id='agentModel'
|
||||
<Select
|
||||
value={agentModel}
|
||||
onChange={(event) => setAgentModel(event.target.value)}
|
||||
/>
|
||||
onValueChange={(value) => setAgentModel(value as AgentModel)}
|
||||
>
|
||||
<SelectTrigger id='agentModel'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modelOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Reasoning effort</Label>
|
||||
@@ -160,27 +183,37 @@ export const SpoonAgentSettingsForm = ({
|
||||
<Input
|
||||
id='installCommand'
|
||||
value={installCommand}
|
||||
placeholder='bun install'
|
||||
placeholder='Auto-detect from lockfile'
|
||||
onChange={(event) => setInstallCommand(event.target.value)}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Leave blank to inspect the repository and choose bun, pnpm, yarn,
|
||||
or npm.
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='checkCommand'>Check command</Label>
|
||||
<Input
|
||||
id='checkCommand'
|
||||
value={checkCommand}
|
||||
placeholder='bun typecheck'
|
||||
placeholder='Auto-detect typecheck or lint'
|
||||
onChange={(event) => setCheckCommand(event.target.value)}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Leave blank to read package.json scripts after cloning.
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='testCommand'>Test command</Label>
|
||||
<Input
|
||||
id='testCommand'
|
||||
value={testCommand}
|
||||
placeholder='bun test'
|
||||
placeholder='Auto-detect test script'
|
||||
onChange={(event) => setTestCommand(event.target.value)}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Leave blank to run the detected test script when one exists.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type='button' onClick={save}>
|
||||
|
||||
@@ -10,49 +10,49 @@ const formatDate = (value?: number) =>
|
||||
: 'Never';
|
||||
|
||||
export const SpoonCard = ({ spoon }: { spoon: Doc<'spoons'> }) => (
|
||||
<Card className='hover:border-primary/50 shadow-none transition-colors'>
|
||||
<CardHeader className='flex-row items-start justify-between gap-4'>
|
||||
<div>
|
||||
<CardTitle className='text-lg'>
|
||||
<Link href={`/spoons/${spoon._id}`} className='hover:underline'>
|
||||
<Link href={`/spoons/${spoon._id}`} className='group/spoon-card block'>
|
||||
<Card className='group-hover/spoon-card:border-primary/50 group-hover/spoon-card:bg-muted/20 shadow-none transition-colors'>
|
||||
<CardHeader className='flex-row items-start justify-between gap-4'>
|
||||
<div>
|
||||
<CardTitle className='group-hover/spoon-card:text-primary text-lg transition-colors'>
|
||||
{spoon.name}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
{spoon.upstreamOwner}/{spoon.upstreamRepo}
|
||||
</p>
|
||||
</div>
|
||||
<SpoonStatusBadge status={spoon.syncStatus ?? spoon.status} />
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-3 text-sm md:grid-cols-2'>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Provider</p>
|
||||
<p className='font-medium'>{spoon.provider}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Cadence</p>
|
||||
<p className='font-medium'>{spoon.syncCadence}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Fork</p>
|
||||
<p className='font-medium'>
|
||||
{spoon.forkOwner && spoon.forkRepo
|
||||
? `${spoon.forkOwner}/${spoon.forkRepo}`
|
||||
: 'Not connected'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Last checked</p>
|
||||
<p className='font-medium'>{formatDate(spoon.lastCheckedAt)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Upstream waiting</p>
|
||||
<p className='font-medium'>{spoon.upstreamAheadBy ?? 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Fork-only commits</p>
|
||||
<p className='font-medium'>{spoon.forkAheadBy ?? 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardTitle>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
{spoon.upstreamOwner}/{spoon.upstreamRepo}
|
||||
</p>
|
||||
</div>
|
||||
<SpoonStatusBadge status={spoon.syncStatus ?? spoon.status} />
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-3 text-sm md:grid-cols-2'>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Provider</p>
|
||||
<p className='font-medium'>{spoon.provider}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Cadence</p>
|
||||
<p className='font-medium'>{spoon.syncCadence}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Fork</p>
|
||||
<p className='font-medium'>
|
||||
{spoon.forkOwner && spoon.forkRepo
|
||||
? `${spoon.forkOwner}/${spoon.forkRepo}`
|
||||
: 'Not connected'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Last checked</p>
|
||||
<p className='font-medium'>{formatDate(spoon.lastCheckedAt)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Upstream waiting</p>
|
||||
<p className='font-medium'>{spoon.upstreamAheadBy ?? 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Fork-only commits</p>
|
||||
<p className='font-medium'>{spoon.forkAheadBy ?? 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
Label,
|
||||
} from '@spoon/ui';
|
||||
@@ -102,6 +109,7 @@ export const SpoonClonePanel = ({ spoon }: { spoon: Doc<'spoons'> }) => {
|
||||
const [remoteName, setRemoteName] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [remoteDialogOpen, setRemoteDialogOpen] = useState(false);
|
||||
const cloneUrl = spoon.forkUrl;
|
||||
|
||||
const addRemote = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -117,6 +125,7 @@ export const SpoonClonePanel = ({ spoon }: { spoon: Doc<'spoons'> }) => {
|
||||
setLabel('');
|
||||
setRemoteName('');
|
||||
setUrl('');
|
||||
setRemoteDialogOpen(false);
|
||||
toast.success('Remote added.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -128,8 +137,64 @@ export const SpoonClonePanel = ({ spoon }: { spoon: Doc<'spoons'> }) => {
|
||||
|
||||
return (
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader className='pb-3'>
|
||||
<CardHeader className='flex-row items-center justify-between gap-4 pb-3'>
|
||||
<CardTitle className='text-base'>Clone your fork</CardTitle>
|
||||
<Dialog open={remoteDialogOpen} onOpenChange={setRemoteDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type='button' variant='outline' size='sm'>
|
||||
<Plus className='size-4' />
|
||||
Add remote
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='sm:max-w-lg'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add git remote</DialogTitle>
|
||||
<DialogDescription>
|
||||
Store another repository URL to copy from this Spoon. GitHub
|
||||
remains the source of truth for upstream maintenance.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={addRemote} className='space-y-4'>
|
||||
<div className='grid gap-3 md:grid-cols-[1fr_0.7fr]'>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='remote-label'>Label</Label>
|
||||
<Input
|
||||
id='remote-label'
|
||||
value={label}
|
||||
placeholder='Gitea mirror'
|
||||
required
|
||||
onChange={(event) => setLabel(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='remote-name'>Git remote name</Label>
|
||||
<Input
|
||||
id='remote-name'
|
||||
value={remoteName}
|
||||
placeholder='gitea'
|
||||
onChange={(event) => setRemoteName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='remote-url'>Repository URL</Label>
|
||||
<Input
|
||||
id='remote-url'
|
||||
value={url}
|
||||
placeholder='https://git.example.com/you/project.git'
|
||||
required
|
||||
onChange={(event) => setUrl(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className='mt-2'>
|
||||
<Button type='submit' disabled={submitting}>
|
||||
<Plus className='size-4' />
|
||||
{submitting ? 'Adding...' : 'Add remote'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-3'>
|
||||
{cloneUrl ? (
|
||||
@@ -165,46 +230,6 @@ export const SpoonClonePanel = ({ spoon }: { spoon: Doc<'spoons'> }) => {
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<form
|
||||
onSubmit={addRemote}
|
||||
className='border-border space-y-3 border-t pt-4'
|
||||
>
|
||||
<div className='grid gap-3 md:grid-cols-[1fr_0.7fr]'>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='remote-label'>Label</Label>
|
||||
<Input
|
||||
id='remote-label'
|
||||
value={label}
|
||||
placeholder='Gitea mirror'
|
||||
required
|
||||
onChange={(event) => setLabel(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='remote-name'>Git remote name</Label>
|
||||
<Input
|
||||
id='remote-name'
|
||||
value={remoteName}
|
||||
placeholder='gitea'
|
||||
onChange={(event) => setRemoteName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='remote-url'>Repository URL</Label>
|
||||
<Input
|
||||
id='remote-url'
|
||||
value={url}
|
||||
placeholder='https://git.example.com/you/project.git'
|
||||
required
|
||||
onChange={(event) => setUrl(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type='submit' variant='outline' disabled={submitting}>
|
||||
<Plus className='size-4' />
|
||||
{submitting ? 'Adding...' : 'Add remote'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -95,6 +95,11 @@ export const SpoonDetailHeader = ({
|
||||
<span>Fork metadata missing</span>
|
||||
)}
|
||||
</div>
|
||||
{spoon.description ? (
|
||||
<p className='text-muted-foreground max-w-3xl text-sm leading-6'>
|
||||
{spoon.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='flex shrink-0 flex-wrap gap-2'>
|
||||
<Button
|
||||
|
||||
@@ -13,9 +13,21 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch,
|
||||
} from '@spoon/ui';
|
||||
|
||||
type MaintenanceMode = 'watch' | 'auto_pr' | 'paused';
|
||||
type SyncCadence = 'daily' | 'weekly' | 'manual';
|
||||
type ProductionRefStrategy =
|
||||
| 'default_branch'
|
||||
| 'latest_release'
|
||||
| 'tag_pattern';
|
||||
|
||||
export const SpoonSettingsForm = ({
|
||||
spoon,
|
||||
settings,
|
||||
@@ -24,6 +36,15 @@ export const SpoonSettingsForm = ({
|
||||
settings?: Doc<'spoonSettings'> | null;
|
||||
}) => {
|
||||
const update = useMutation(api.spoonSettings.update);
|
||||
const updateSpoonSettings = useMutation(api.spoons.updateSettings);
|
||||
const [syncCadence, setSyncCadence] = useState<SyncCadence>(
|
||||
spoon.syncCadence,
|
||||
);
|
||||
const [maintenanceMode, setMaintenanceMode] = useState<MaintenanceMode>(
|
||||
spoon.maintenanceMode,
|
||||
);
|
||||
const [productionRefStrategy, setProductionRefStrategy] =
|
||||
useState<ProductionRefStrategy>(spoon.productionRefStrategy);
|
||||
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(
|
||||
settings?.autoRefreshEnabled ?? true,
|
||||
);
|
||||
@@ -42,14 +63,22 @@ export const SpoonSettingsForm = ({
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await update({
|
||||
spoonId: spoon._id,
|
||||
autoRefreshEnabled,
|
||||
autoReviewEnabled,
|
||||
autoSyncEnabled,
|
||||
requireAiLowRiskForSync,
|
||||
requireCleanCompareForSync,
|
||||
});
|
||||
await Promise.all([
|
||||
updateSpoonSettings({
|
||||
spoonId: spoon._id,
|
||||
syncCadence,
|
||||
maintenanceMode,
|
||||
productionRefStrategy,
|
||||
}),
|
||||
update({
|
||||
spoonId: spoon._id,
|
||||
autoRefreshEnabled,
|
||||
autoReviewEnabled,
|
||||
autoSyncEnabled,
|
||||
requireAiLowRiskForSync,
|
||||
requireCleanCompareForSync,
|
||||
}),
|
||||
]);
|
||||
toast.success('Spoon settings saved.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -92,21 +121,57 @@ export const SpoonSettingsForm = ({
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='grid gap-3 md:grid-cols-2'>
|
||||
<div>
|
||||
<p className='text-muted-foreground text-xs'>Sync cadence</p>
|
||||
<p className='font-medium'>{spoon.syncCadence}</p>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Sync cadence</Label>
|
||||
<Select
|
||||
value={syncCadence}
|
||||
onValueChange={(value) => setSyncCadence(value as SyncCadence)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='daily'>Daily</SelectItem>
|
||||
<SelectItem value='weekly'>Weekly</SelectItem>
|
||||
<SelectItem value='manual'>Manual</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground text-xs'>Maintenance mode</p>
|
||||
<p className='font-medium'>
|
||||
{spoon.maintenanceMode.replaceAll('_', ' ')}
|
||||
</p>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Maintenance mode</Label>
|
||||
<Select
|
||||
value={maintenanceMode}
|
||||
onValueChange={(value) =>
|
||||
setMaintenanceMode(value as MaintenanceMode)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='watch'>Watch only</SelectItem>
|
||||
<SelectItem value='auto_pr'>Prepare merge requests</SelectItem>
|
||||
<SelectItem value='paused'>Paused</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground text-xs'>Production ref</p>
|
||||
<p className='font-medium'>
|
||||
{spoon.productionRefStrategy.replaceAll('_', ' ')}
|
||||
</p>
|
||||
<div className='grid gap-2'>
|
||||
<Label>Production ref</Label>
|
||||
<Select
|
||||
value={productionRefStrategy}
|
||||
onValueChange={(value) =>
|
||||
setProductionRefStrategy(value as ProductionRefStrategy)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='default_branch'>Default branch</SelectItem>
|
||||
<SelectItem value='latest_release'>Latest release</SelectItem>
|
||||
<SelectItem value='tag_pattern'>Tag pattern</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-3'>
|
||||
|
||||
Reference in new issue
Block a user