Move to threads based system.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user