Update stuff
Build and Push Next App / quality (push) Successful in 1m21s
Build and Push Next App / build-next (push) Successful in 3m34s

This commit is contained in:
Gabriel Brown committed 2026-06-22 00:41:51 -05:00
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}>
+44 -44
View File
@@ -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'>