Move to threads based system.

This commit is contained in:
Gabriel Brown
2026-06-22 10:37:26 -04:00
parent 8ae6c4b533
commit 206b64176b
82 changed files with 6169 additions and 1930 deletions
@@ -0,0 +1,153 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@spoon/ui';
import type { DiffResponse, FileResponse, FileTreeNode } from './types';
import { AgentThread } from './agent-thread';
import { CodeEditor } from './code-editor';
import { CommandPanel } from './command-panel';
import { DiffViewer } from './diff-viewer';
import { FileTree } from './file-tree';
import { JobStatusBar } from './job-status-bar';
import { WorkspaceActions } from './workspace-actions';
export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
const job = useQuery(api.agentJobs.get, { jobId });
const messages =
useQuery(api.agentJobs.listMessages, { jobId, limit: 200 }) ?? [];
const [tree, setTree] = useState<FileTreeNode | null>(null);
const [selectedPath, setSelectedPath] = useState<string>();
const [fileContent, setFileContent] = useState('');
const [diff, setDiff] = useState('');
const workspaceDisabled =
!job ||
['draft_pr_opened', 'failed', 'cancelled', 'timed_out'].includes(
job.status,
) ||
['stopped', 'expired', 'failed'].includes(job.workspaceStatus ?? '');
const loadTree = useCallback(async () => {
const response = await fetch(`/api/agent-jobs/${jobId}/tree`);
if (!response.ok) throw new Error(await response.text());
const data = (await response.json()) as { tree: FileTreeNode | null };
setTree(data.tree);
}, [jobId]);
const loadDiff = useCallback(async () => {
const response = await fetch(`/api/agent-jobs/${jobId}/diff`);
if (!response.ok) throw new Error(await response.text());
const data = (await response.json()) as DiffResponse;
setDiff(data.diff);
}, [jobId]);
const loadFile = useCallback(
async (path: string) => {
const response = await fetch(
`/api/agent-jobs/${jobId}/file?path=${encodeURIComponent(path)}`,
);
if (!response.ok) throw new Error(await response.text());
const data = (await response.json()) as FileResponse;
setSelectedPath(data.path);
setFileContent(data.content);
},
[jobId],
);
useEffect(() => {
if (!job) return;
const timeout = window.setTimeout(() => {
void loadTree().catch((error: unknown) => {
console.error(error);
});
void loadDiff().catch((error: unknown) => {
console.error(error);
});
}, 0);
return () => window.clearTimeout(timeout);
}, [job, loadDiff, loadTree]);
if (job === undefined) {
return (
<main className='text-muted-foreground p-6'>Loading workspace...</main>
);
}
const saveFile = async (content: string) => {
if (!selectedPath) return;
const response = await fetch(`/api/agent-jobs/${jobId}/file`, {
method: 'PUT',
body: JSON.stringify({ path: selectedPath, content }),
});
if (!response.ok) {
toast.error('Could not save file.');
throw new Error(await response.text());
}
setFileContent(content);
await loadDiff();
toast.success('File saved.');
};
return (
<main className='border-border bg-muted/20 min-h-[calc(100vh-5rem)] overflow-hidden rounded-md border'>
<JobStatusBar job={job} />
<div className='border-border bg-background flex items-center justify-end border-b px-4 py-2'>
<WorkspaceActions job={job} disabled={workspaceDisabled} />
</div>
<div className='grid min-h-[680px] grid-cols-1 xl:grid-cols-[260px_minmax(0,1fr)_360px]'>
<aside className='border-border bg-background min-h-[260px] border-r'>
<div className='border-border border-b p-3'>
<h2 className='text-sm font-semibold'>Files</h2>
<p className='text-muted-foreground text-xs'>Current workspace</p>
</div>
<FileTree
tree={tree}
selectedPath={selectedPath}
onSelect={(path) => {
void loadFile(path).catch((error) => {
console.error(error);
toast.error('Could not load file.');
});
}}
/>
</aside>
<section className='bg-background min-w-0'>
<Tabs defaultValue='editor' className='h-full'>
<TabsList
variant='line'
className='border-border h-11 w-full justify-start rounded-none border-b px-3'
>
<TabsTrigger value='editor'>Editor</TabsTrigger>
<TabsTrigger value='diff'>Diff</TabsTrigger>
</TabsList>
<TabsContent value='editor' className='m-0'>
<CodeEditor
path={selectedPath}
content={fileContent}
readOnly={workspaceDisabled}
onSave={saveFile}
/>
</TabsContent>
<TabsContent value='diff' className='m-0'>
<DiffViewer diff={diff} onRefresh={loadDiff} />
</TabsContent>
</Tabs>
<CommandPanel jobId={jobId} disabled={workspaceDisabled} />
</section>
<aside className='border-border bg-muted/20 min-w-0 border-l'>
<AgentThread
jobId={jobId}
messages={messages}
disabled={workspaceDisabled}
/>
</aside>
</div>
</main>
);
};