Add features & update project
This commit is contained in:
@@ -6,7 +6,7 @@ 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 { Button, Tabs, TabsContent, TabsList, TabsTrigger } from '@spoon/ui';
|
||||
|
||||
import type { DiffResponse, FileResponse, FileTreeNode } from './types';
|
||||
import { AgentThread } from './agent-thread';
|
||||
@@ -40,6 +40,9 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
}) ?? [];
|
||||
const uiState = useQuery(api.agentJobs.getWorkspaceUiState, { jobId });
|
||||
const patchUiState = useMutation(api.agentJobs.patchWorkspaceUiState);
|
||||
const createJobForThread = useMutation(api.agentJobs.createForThread);
|
||||
const deleteWorkspace = useMutation(api.agentJobs.deleteWorkspace);
|
||||
const markWorkspaceLost = useMutation(api.agentJobs.markWorkspaceLost);
|
||||
const [tree, setTree] = useState<FileTreeNode | null>(null);
|
||||
const [files, setFiles] = useState<Record<string, OpenFileState>>({});
|
||||
const [openFilePaths, setOpenFilePaths] = useState<string[]>([]);
|
||||
@@ -50,6 +53,8 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
const [vimEnabled, setVimEnabled] = useState(false);
|
||||
const [hydratedUiState, setHydratedUiState] = useState(false);
|
||||
const [diff, setDiff] = useState('');
|
||||
const [workspaceError, setWorkspaceError] = useState<string>();
|
||||
const [agentTurnActive, setAgentTurnActive] = useState(false);
|
||||
|
||||
const workspaceDisabled =
|
||||
!job ||
|
||||
@@ -62,6 +67,7 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
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 };
|
||||
setWorkspaceError(undefined);
|
||||
setTree(data.tree);
|
||||
}, [jobId]);
|
||||
|
||||
@@ -69,9 +75,20 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
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;
|
||||
setWorkspaceError(undefined);
|
||||
setDiff(data.diff);
|
||||
}, [jobId]);
|
||||
|
||||
const loadAgentStatus = useCallback(async () => {
|
||||
const response = await fetch(`/api/agent-jobs/${jobId}/agent/status`);
|
||||
if (!response.ok) {
|
||||
setAgentTurnActive(false);
|
||||
return;
|
||||
}
|
||||
const data = (await response.json()) as { active?: boolean };
|
||||
setAgentTurnActive(Boolean(data.active));
|
||||
}, [jobId]);
|
||||
|
||||
const loadFile = useCallback(
|
||||
async (path: string) => {
|
||||
setFiles((current) => ({
|
||||
@@ -132,13 +149,27 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
void loadTree().catch((error: unknown) => {
|
||||
console.error(error);
|
||||
setWorkspaceError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
});
|
||||
void loadDiff().catch((error: unknown) => {
|
||||
console.error(error);
|
||||
setWorkspaceError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
});
|
||||
void loadAgentStatus();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [job, loadDiff, loadTree]);
|
||||
}, [job, loadAgentStatus, loadDiff, loadTree]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setInterval(() => {
|
||||
void loadAgentStatus();
|
||||
}, 5_000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [loadAgentStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uiState || hydratedUiState) return;
|
||||
@@ -197,6 +228,23 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
}
|
||||
|
||||
const activeFile = activeFilePath ? files[activeFilePath] : undefined;
|
||||
const recoverWorkspace = async () => {
|
||||
if (!job.threadId) return;
|
||||
const newJobId = await createJobForThread({
|
||||
threadId: job.threadId,
|
||||
jobType: job.jobType ?? 'user_change',
|
||||
});
|
||||
window.location.href = `/spoons/${job.spoonId}/agent/${newJobId}`;
|
||||
};
|
||||
|
||||
const deleteStaleWorkspace = async () => {
|
||||
if (!window.confirm('Delete this stale workspace record?')) return;
|
||||
await markWorkspaceLost({ jobId });
|
||||
await deleteWorkspace({ jobId });
|
||||
window.location.href = job.threadId
|
||||
? `/threads/${job.threadId}`
|
||||
: `/spoons/${job.spoonId}`;
|
||||
};
|
||||
|
||||
const saveFile = async (content: string) => {
|
||||
if (!activeFilePath) return;
|
||||
@@ -280,6 +328,35 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
return (
|
||||
<main className='border-border bg-muted/20 flex h-[calc(100vh-8.5rem)] min-h-[720px] flex-col overflow-hidden rounded-md border'>
|
||||
<JobStatusBar job={job} />
|
||||
{workspaceError ? (
|
||||
<div className='border-border bg-background border-b p-4'>
|
||||
<div className='border-destructive/40 bg-destructive/5 rounded-md border p-4'>
|
||||
<p className='font-medium'>Workspace not active on this worker</p>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
{workspaceError}
|
||||
</p>
|
||||
<div className='mt-3 flex flex-wrap gap-2'>
|
||||
{job.threadId ? (
|
||||
<Button type='button' onClick={() => void recoverWorkspace()}>
|
||||
Recreate workspace run
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => void deleteStaleWorkspace()}
|
||||
>
|
||||
Delete stale workspace
|
||||
</Button>
|
||||
{job.threadId ? (
|
||||
<Button type='button' variant='outline' asChild>
|
||||
<a href={`/threads/${job.threadId}`}>Open thread</a>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className='border-border bg-background flex items-center justify-end border-b px-4 py-2'>
|
||||
<WorkspaceActions job={job} disabled={workspaceDisabled} />
|
||||
</div>
|
||||
@@ -362,6 +439,7 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
events={events}
|
||||
interactions={interactions}
|
||||
disabled={workspaceDisabled}
|
||||
agentTurnActive={agentTurnActive}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -374,6 +452,7 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
events={events}
|
||||
interactions={interactions}
|
||||
disabled={workspaceDisabled}
|
||||
agentTurnActive={agentTurnActive}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user