Initial commit for project Spoon!
Build and Push Next App / quality (push) Failing after 45s
Build and Push Next App / build-next (push) Has been skipped

This commit is contained in:
Gabriel Brown
2026-06-21 17:52:02 -05:00
commit cf7ff2ee4e
268 changed files with 32981 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
'use client';
import { useState } from 'react';
import { useMutation, useQuery } from 'convex/react';
import { toast } from 'sonner';
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
import { api } from '@spoon/backend/convex/_generated/api.js';
import {
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
} from '@spoon/ui';
const AgentsPage = () => {
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
const requests = useQuery(api.agentRequests.listRecent, { limit: 50 }) ?? [];
const createRequest = useMutation(api.agentRequests.create);
const [spoonId, setSpoonId] = useState('');
const [targetBranch, setTargetBranch] = useState('');
const [prompt, setPrompt] = useState('');
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!spoonId) {
toast.error('Choose a Spoon first.');
return;
}
setSubmitting(true);
try {
await createRequest({
spoonId: spoonId as Id<'spoons'>,
prompt,
targetBranch: targetBranch || undefined,
});
setPrompt('');
setTargetBranch('');
toast.success('Agent request queued.');
} catch (error) {
console.error(error);
toast.error('Could not queue agent request.');
} finally {
setSubmitting(false);
}
};
return (
<main className='space-y-6'>
<div>
<h1 className='text-3xl font-semibold tracking-normal'>Agents</h1>
<p className='text-muted-foreground mt-2'>
Queue prompt-driven work for future AI merge request automation.
</p>
</div>
<div className='grid gap-6 xl:grid-cols-[0.9fr_1.1fr]'>
<Card className='shadow-none'>
<CardHeader>
<CardTitle>Request work</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className='space-y-4'>
<div className='grid gap-2'>
<Label>Spoon</Label>
<Select value={spoonId} onValueChange={setSpoonId}>
<SelectTrigger className='w-full'>
<SelectValue placeholder='Choose a Spoon' />
</SelectTrigger>
<SelectContent>
{spoons.map((spoon) => (
<SelectItem key={spoon._id} value={spoon._id}>
{spoon.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='grid gap-2'>
<Label htmlFor='targetBranch'>Target branch</Label>
<Input
id='targetBranch'
value={targetBranch}
placeholder='feature/my-change'
onChange={(event) => setTargetBranch(event.target.value)}
/>
</div>
<div className='grid gap-2'>
<Label htmlFor='prompt'>Prompt</Label>
<Textarea
id='prompt'
value={prompt}
required
onChange={(event) => setPrompt(event.target.value)}
/>
</div>
<Button type='submit' disabled={submitting || !spoons.length}>
{submitting ? 'Queueing...' : 'Queue request'}
</Button>
</form>
</CardContent>
</Card>
<Card className='shadow-none'>
<CardHeader>
<CardTitle>Recent requests</CardTitle>
</CardHeader>
<CardContent>
{requests.length ? (
<div className='space-y-3'>
{requests.map((request) => (
<div key={request._id} className='border-border border p-4'>
<p className='font-medium'>{request.prompt}</p>
<p className='text-muted-foreground mt-1 text-sm'>
{request.status.replaceAll('_', ' ')}
</p>
</div>
))}
</div>
) : (
<p className='text-muted-foreground'>
Agent requests will appear here after you create a Spoon and
queue work.
</p>
)}
</CardContent>
</Card>
</div>
</main>
);
};
export default AgentsPage;
+121
View File
@@ -0,0 +1,121 @@
'use client';
import Link from 'next/link';
import { MetricCard } from '@/components/dashboard/metric-card';
import { SpoonCard } from '@/components/spoons/spoon-card';
import { useQuery } from 'convex/react';
import { Bot, GitBranch, GitPullRequest, RefreshCw } from 'lucide-react';
import { api } from '@spoon/backend/convex/_generated/api.js';
import { Button, Card, CardContent, CardHeader, CardTitle } from '@spoon/ui';
const DashboardPage = () => {
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
const syncRuns = useQuery(api.syncRuns.listRecent, { limit: 5 }) ?? [];
const agentRequests =
useQuery(api.agentRequests.listRecent, { limit: 5 }) ?? [];
const activeSpoons = spoons.filter(
(spoon) => spoon.status === 'active',
).length;
const needsReview = syncRuns.filter(
(run) => run.status === 'needs_review',
).length;
return (
<main className='space-y-6'>
<div className='flex flex-col justify-between gap-4 md:flex-row md:items-end'>
<div>
<h1 className='text-3xl font-semibold tracking-normal'>Dashboard</h1>
<p className='text-muted-foreground mt-2'>
Monitor managed forks, upstream activity, and queued agent work.
</p>
</div>
<Button asChild>
<Link href='/spoons/new'>Create Spoon</Link>
</Button>
</div>
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<MetricCard
label='Total Spoons'
value={spoons.length}
note='Managed forks'
icon={GitBranch}
/>
<MetricCard
label='Active Spoons'
value={activeSpoons}
note='Ready for checks'
icon={GitPullRequest}
/>
<MetricCard
label='Needs review'
value={needsReview}
note='Upstream updates'
icon={RefreshCw}
/>
<MetricCard
label='Agent requests'
value={agentRequests.length}
note='Queued and recent'
icon={Bot}
/>
</div>
<div className='grid gap-6 xl:grid-cols-2'>
<section className='space-y-3'>
<h2 className='text-lg font-semibold'>Recent Spoons</h2>
{spoons.length ? (
spoons
.slice(0, 3)
.map((spoon) => <SpoonCard key={spoon._id} spoon={spoon} />)
) : (
<Card className='shadow-none'>
<CardContent className='p-6'>
<p className='font-medium'>No Spoons yet</p>
<p className='text-muted-foreground mt-2 text-sm'>
Create a manual Spoon record to start shaping your fork
maintenance dashboard.
</p>
</CardContent>
</Card>
)}
</section>
<section className='space-y-3'>
<h2 className='text-lg font-semibold'>Recent activity</h2>
<Card className='shadow-none'>
<CardHeader>
<CardTitle className='text-base'>Upstream checks</CardTitle>
</CardHeader>
<CardContent>
{syncRuns.length ? (
<div className='space-y-3'>
{syncRuns.map((run) => (
<div
key={run._id}
className='border-border border p-3 text-sm'
>
<p className='font-medium'>
{run.kind.replaceAll('_', ' ')}
</p>
<p className='text-muted-foreground'>
{run.status.replaceAll('_', ' ')}
</p>
</div>
))}
</div>
) : (
<p className='text-muted-foreground text-sm'>
Scheduled upstream checks will appear here once provider
automation is connected.
</p>
)}
</CardContent>
</Card>
</section>
</div>
</main>
);
};
export default DashboardPage;
+8
View File
@@ -0,0 +1,8 @@
import type { ReactNode } from 'react';
import { AppShell } from '@/components/app-shell/app-shell';
const Layout = ({ children }: { children: ReactNode }) => (
<AppShell>{children}</AppShell>
);
export default Layout;
@@ -0,0 +1,16 @@
import { NewSpoonForm } from '@/components/spoons/new-spoon-form';
const NewSpoonPage = () => (
<main className='space-y-6'>
<div>
<h1 className='text-3xl font-semibold tracking-normal'>New Spoon</h1>
<p className='text-muted-foreground mt-2 max-w-2xl'>
Create a provider-neutral managed fork record. This does not call a Git
provider yet; it prepares the dashboard surface for future automation.
</p>
</div>
<NewSpoonForm />
</main>
);
export default NewSpoonPage;
+49
View File
@@ -0,0 +1,49 @@
'use client';
import Link from 'next/link';
import { SpoonCard } from '@/components/spoons/spoon-card';
import { useQuery } from 'convex/react';
import { api } from '@spoon/backend/convex/_generated/api.js';
import { Button, Card, CardContent } from '@spoon/ui';
const SpoonsPage = () => {
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
return (
<main className='space-y-6'>
<div className='flex flex-col justify-between gap-4 md:flex-row md:items-end'>
<div>
<h1 className='text-3xl font-semibold tracking-normal'>My Spoons</h1>
<p className='text-muted-foreground mt-2'>
Managed forks you want to keep close to their upstream projects.
</p>
</div>
<Button asChild>
<Link href='/spoons/new'>New Spoon</Link>
</Button>
</div>
{spoons.length ? (
<div className='grid gap-4 xl:grid-cols-2'>
{spoons.map((spoon) => (
<SpoonCard key={spoon._id} spoon={spoon} />
))}
</div>
) : (
<Card className='shadow-none'>
<CardContent className='p-8'>
<p className='text-lg font-medium'>No managed forks yet</p>
<p className='text-muted-foreground mt-2 max-w-xl'>
Add your first Spoon manually. Provider-backed forking can build
on this same record later.
</p>
<Button className='mt-5' asChild>
<Link href='/spoons/new'>Create Spoon</Link>
</Button>
</CardContent>
</Card>
)}
</main>
);
};
export default SpoonsPage;
+83
View File
@@ -0,0 +1,83 @@
'use client';
import { useQuery } from 'convex/react';
import { api } from '@spoon/backend/convex/_generated/api.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@spoon/ui';
const UpdatesPage = () => {
const runs = useQuery(api.syncRuns.listRecent, { limit: 50 }) ?? [];
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
return (
<main className='space-y-6'>
<div>
<h1 className='text-3xl font-semibold tracking-normal'>Updates</h1>
<p className='text-muted-foreground mt-2'>
Upstream checks, merge attempts, and AI reviews will appear here.
</p>
</div>
<div className='flex flex-col gap-3 md:flex-row'>
<Select defaultValue='all'>
<SelectTrigger className='w-full md:w-48'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>All statuses</SelectItem>
<SelectItem value='needs_review'>Needs review</SelectItem>
<SelectItem value='conflict'>Conflict</SelectItem>
<SelectItem value='clean'>Clean</SelectItem>
</SelectContent>
</Select>
<Select defaultValue='all'>
<SelectTrigger className='w-full md:w-64'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>All Spoons</SelectItem>
{spoons.map((spoon) => (
<SelectItem key={spoon._id} value={spoon._id}>
{spoon.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Card className='shadow-none'>
<CardHeader>
<CardTitle>Recent sync runs</CardTitle>
</CardHeader>
<CardContent>
{runs.length ? (
<div className='space-y-3'>
{runs.map((run) => (
<div key={run._id} className='border-border border p-4'>
<p className='font-medium'>{run.kind.replaceAll('_', ' ')}</p>
<p className='text-muted-foreground text-sm'>
{run.status.replaceAll('_', ' ')}
</p>
</div>
))}
</div>
) : (
<p className='text-muted-foreground'>
Scheduled upstream checks will appear here once provider
connections and workers are added.
</p>
)}
</CardContent>
</Card>
</main>
);
};
export default UpdatesPage;