From a27207b75e99d1dae8d0355f8b598ef1c78894ec Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Tue, 15 Jul 2025 08:18:05 +0200 Subject: [PATCH] Normalise API calls on FE (#173) * tiny fix * Move all api calls from components to lib/api.ts (vibe-kanban) (#165) * unify loaders * simplify scroll to bottom logic for logs * better key for display entry * finish normalising api calls * remove withErrorHandling function * cleanup --- frontend/src/App.tsx | 75 +-- frontend/src/components/GitHubLoginDialog.tsx | 61 +- frontend/src/components/config-provider.tsx | 51 +- .../context/TaskDetailsContextProvider.tsx | 211 +++---- .../components/projects/project-detail.tsx | 48 +- .../src/components/projects/project-form.tsx | 38 +- .../src/components/projects/project-list.tsx | 53 +- .../tasks/DeleteFileConfirmationDialog.tsx | 37 +- .../components/tasks/TaskActivityHistory.tsx | 2 +- .../components/tasks/TaskDetails/DiffCard.tsx | 6 +- .../components/tasks/TaskDetails/DiffTab.tsx | 4 +- .../components/tasks/TaskDetails/LogsTab.tsx | 71 +-- .../{ => LogsTab}/Conversation.tsx | 35 +- .../NormalizedConversationViewer.tsx | 53 +- .../LogsTab/SetupScriptRunning.tsx | 49 ++ .../src/components/tasks/TaskDetailsPanel.tsx | 9 +- .../components/tasks/TaskDetailsToolbar.tsx | 88 ++- .../components/tasks/TaskFollowUpSection.tsx | 42 +- .../tasks/Toolbar/CreateAttempt.tsx | 25 +- .../tasks/Toolbar/CreatePRDialog.tsx | 76 ++- .../tasks/Toolbar/CurrentAttempt.tsx | 149 ++--- .../components/ui/file-search-textarea.tsx | 20 +- frontend/src/components/ui/folder-picker.tsx | 36 +- frontend/src/components/ui/loader.tsx | 24 + frontend/src/lib/api.ts | 546 ++++++++++++++++++ frontend/src/pages/McpServers.tsx | 75 +-- frontend/src/pages/project-tasks.tsx | 206 ++----- shared/types.ts | 270 ++++++++- 28 files changed, 1349 insertions(+), 1011 deletions(-) rename frontend/src/components/tasks/TaskDetails/{ => LogsTab}/Conversation.tsx (85%) rename frontend/src/components/tasks/TaskDetails/{ => LogsTab}/NormalizedConversationViewer.tsx (86%) create mode 100644 frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx create mode 100644 frontend/src/components/ui/loader.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 24316f00..0dd50243 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { Navbar } from '@/components/layout/navbar'; import { Projects } from '@/pages/projects'; import { ProjectTasks } from '@/pages/project-tasks'; @@ -11,13 +11,10 @@ import { OnboardingDialog } from '@/components/OnboardingDialog'; import { PrivacyOptInDialog } from '@/components/PrivacyOptInDialog'; import { ConfigProvider, useConfig } from '@/components/config-provider'; import { ThemeProvider } from '@/components/theme-provider'; -import type { - Config, - ApiResponse, - ExecutorConfig, - EditorType, -} from 'shared/types'; +import type { EditorType, ExecutorConfig } from 'shared/types'; +import { configApi } from '@/lib/api'; import * as Sentry from '@sentry/react'; +import { Loader } from '@/components/ui/loader'; import { GitHubLoginDialog } from '@/components/GitHubLoginDialog'; const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); @@ -44,7 +41,7 @@ function AppContent() { if (config.telemetry_acknowledged) { const notAuthenticated = !config.github?.username || !config.github?.token; - setShowGitHubLogin(notAuthenticated || githubTokenInvalid); + setShowGitHubLogin(notAuthenticated); } else { setShowGitHubLogin(false); } @@ -60,20 +57,9 @@ function AppContent() { updateConfig({ disclaimer_acknowledged: true }); try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ ...config, disclaimer_acknowledged: true }), - }); - - const data: ApiResponse = await response.json(); - - if (data.success) { - setShowDisclaimer(false); - setShowOnboarding(!config.onboarding_acknowledged); - } + await configApi.saveConfig({ ...config, disclaimer_acknowledged: true }); + setShowDisclaimer(false); + setShowOnboarding(!config.onboarding_acknowledged); } catch (err) { console.error('Error saving config:', err); } @@ -95,19 +81,8 @@ function AppContent() { updateConfig(updatedConfig); try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(updatedConfig), - }); - - const data: ApiResponse = await response.json(); - - if (data.success) { - setShowOnboarding(false); - } + await configApi.saveConfig(updatedConfig); + setShowOnboarding(false); } catch (err) { console.error('Error saving config:', err); } @@ -125,23 +100,12 @@ function AppContent() { updateConfig(updatedConfig); try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(updatedConfig), - }); - - const data: ApiResponse = await response.json(); - - if (data.success) { - setShowPrivacyOptIn(false); - // Now show GitHub login after privacy choice is made - const notAuthenticated = - !updatedConfig.github?.username || !updatedConfig.github?.token; - setShowGitHubLogin(notAuthenticated); - } + await configApi.saveConfig(updatedConfig); + setShowPrivacyOptIn(false); + // Now show GitHub login after privacy choice is made + const notAuthenticated = + !updatedConfig.github?.username || !updatedConfig.github?.token; + setShowGitHubLogin(notAuthenticated); } catch (err) { console.error('Error saving config:', err); } @@ -150,10 +114,7 @@ function AppContent() { if (loading) { return (
-
-
-

Loading...

-
+
); } diff --git a/frontend/src/components/GitHubLoginDialog.tsx b/frontend/src/components/GitHubLoginDialog.tsx index 8c52b642..0c414333 100644 --- a/frontend/src/components/GitHubLoginDialog.tsx +++ b/frontend/src/components/GitHubLoginDialog.tsx @@ -10,6 +10,9 @@ import { import { Button } from './ui/button'; import { useConfig } from './config-provider'; import { Check, Clipboard } from 'lucide-react'; +import { Loader } from './ui/loader'; +import { githubAuthApi } from '../lib/api'; +import { StartGitHubDeviceFlowType } from 'shared/types.ts'; export function GitHubLoginDialog({ open, @@ -21,13 +24,8 @@ export function GitHubLoginDialog({ const { config, loading, githubTokenInvalid } = useConfig(); const [fetching, setFetching] = useState(false); const [error, setError] = useState(null); - const [deviceState, setDeviceState] = useState(null); + const [deviceState, setDeviceState] = + useState(null); const [polling, setPolling] = useState(false); const [copied, setCopied] = useState(false); @@ -40,19 +38,12 @@ export function GitHubLoginDialog({ setError(null); setDeviceState(null); try { - const res = await fetch('/api/auth/github/device/start', { - method: 'POST', - }); - const data = await res.json(); - if (data.success && data.data) { - setDeviceState(data.data); - setPolling(true); - } else { - setError(data.message || 'Failed to start GitHub login.'); - } - } catch (e) { + const data = await githubAuthApi.start(); + setDeviceState(data); + setPolling(true); + } catch (e: any) { console.error(e); - setError('Network error'); + setError(e?.message || 'Network error'); } finally { setFetching(false); } @@ -64,35 +55,25 @@ export function GitHubLoginDialog({ if (polling && deviceState) { const poll = async () => { try { - const res = await fetch('/api/auth/github/device/poll', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ device_code: deviceState.device_code }), - }); - const data = await res.json(); - if (data.success) { - setPolling(false); - setDeviceState(null); - setError(null); - window.location.reload(); // reload config - } else if (data.message === 'authorization_pending') { - // keep polling + await githubAuthApi.poll(deviceState.device_code); + setPolling(false); + setDeviceState(null); + setError(null); + window.location.reload(); // reload config + } catch (e: any) { + if (e?.message === 'authorization_pending') { timer = setTimeout(poll, (deviceState.interval || 5) * 1000); - } else if (data.message === 'slow_down') { - // increase interval + } else if (e?.message === 'slow_down') { timer = setTimeout(poll, (deviceState.interval + 5) * 1000); - } else if (data.message === 'expired_token') { + } else if (e?.message === 'expired_token') { setPolling(false); setError('Device code expired. Please try again.'); setDeviceState(null); } else { setPolling(false); - setError(data.message || 'Login failed.'); + setError(e?.message || 'Login failed.'); setDeviceState(null); } - } catch (e) { - setPolling(false); - setError('Network error'); } }; timer = setTimeout(poll, deviceState.interval * 1000); @@ -149,7 +130,7 @@ export function GitHubLoginDialog({ {loading ? ( -
Loading…
+ ) : isAuthenticated ? (
diff --git a/frontend/src/components/config-provider.tsx b/frontend/src/components/config-provider.tsx index d8ba7caa..72a9f627 100644 --- a/frontend/src/components/config-provider.tsx +++ b/frontend/src/components/config-provider.tsx @@ -6,7 +6,8 @@ import { useEffect, useState, } from 'react'; -import type { ApiResponse, Config } from 'shared/types'; +import type { Config } from 'shared/types'; +import { configApi, githubAuthApi } from '../lib/api'; interface ConfigContextType { config: Config | null; @@ -31,12 +32,8 @@ export function ConfigProvider({ children }: ConfigProviderProps) { useEffect(() => { const loadConfig = async () => { try { - const response = await fetch('/api/config'); - const data: ApiResponse = await response.json(); - - if (data.success && data.data) { - setConfig(data.data); - } + const config = await configApi.getConfig(); + setConfig(config); } catch (err) { console.error('Error loading config:', err); } finally { @@ -51,13 +48,12 @@ export function ConfigProvider({ children }: ConfigProviderProps) { useEffect(() => { if (loading) return; const checkToken = async () => { - const response = await fetch('/api/auth/github/check'); - const data: ApiResponse = await response.json(); - if (!data.success && data.message === 'github_token_invalid') { - setGithubTokenInvalid(true); - } else { - setGithubTokenInvalid(false); + const valid = await githubAuthApi.checkGithubToken(); + if (valid === undefined) { + // Network/server error: do not update githubTokenInvalid + return; } + setGithubTokenInvalid(!valid); }; checkToken(); }, [loading]); @@ -68,18 +64,9 @@ export function ConfigProvider({ children }: ConfigProviderProps) { const saveConfig = useCallback(async (): Promise => { if (!config) return false; - try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(config), - }); - - const data: ApiResponse = await response.json(); - return data.success; + await configApi.saveConfig(config); + return true; } catch (err) { console.error('Error saving config:', err); return false; @@ -92,19 +79,11 @@ export function ConfigProvider({ children }: ConfigProviderProps) { const newConfig: Config | null = config ? { ...config, ...updates } : null; - try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(newConfig), - }); - - const data: ApiResponse = await response.json(); - setConfig(data.data); - return data.success; + if (!newConfig) return false; + const saved = await configApi.saveConfig(newConfig); + setConfig(saved); + return true; } catch (err) { console.error('Error saving config:', err); return false; diff --git a/frontend/src/components/context/TaskDetailsContextProvider.tsx b/frontend/src/components/context/TaskDetailsContextProvider.tsx index fd8e1c3b..68a83f00 100644 --- a/frontend/src/components/context/TaskDetailsContextProvider.tsx +++ b/frontend/src/components/context/TaskDetailsContextProvider.tsx @@ -10,18 +10,15 @@ import { useState, } from 'react'; import type { - ApiResponse, AttemptData, EditorType, ExecutionProcess, - ExecutionProcessSummary, TaskAttempt, - TaskAttemptActivityWithPrompt, TaskAttemptState, TaskWithAttemptStatus, WorktreeDiff, } from 'shared/types.ts'; -import { makeRequest } from '@/lib/api.ts'; +import { attemptsApi, executionProcessesApi } from '@/lib/api.ts'; import { TaskAttemptDataContext, TaskAttemptLoadingContext, @@ -41,7 +38,6 @@ const TaskDetailsProvider: FC<{ activeTab: 'logs' | 'diffs'; setActiveTab: Dispatch>; setShowEditorDialog: Dispatch>; - isOpen: boolean; userSelectedTab: boolean; projectHasDevScript?: boolean; }> = ({ @@ -51,7 +47,6 @@ const TaskDetailsProvider: FC<{ activeTab, setActiveTab, setShowEditorDialog, - isOpen, userSelectedTab, projectHasDevScript, }) => { @@ -94,29 +89,26 @@ const TaskDetailsProvider: FC<{ return; } + diffLoadingRef.current = true; + if (isBackgroundRefresh) { + setIsBackgroundRefreshing(true); + } else { + setDiffLoading(true); + } + setDiffError(null); + try { - diffLoadingRef.current = true; - if (isBackgroundRefresh) { - setIsBackgroundRefreshing(true); - } else { - setDiffLoading(true); - } - setDiffError(null); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/diff` + const result = await attemptsApi.getDiff( + projectId, + selectedAttempt.task_id, + selectedAttempt.id ); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setDiff(result.data); - } else { - setDiffError('Failed to load diff'); - } - } else { - setDiffError('Failed to load diff'); + if (result !== undefined) { + setDiff(result); } } catch (err) { + console.error('Failed to load diff:', err); setDiffError('Failed to load diff'); } finally { diffLoadingRef.current = false; @@ -131,29 +123,21 @@ const TaskDetailsProvider: FC<{ ); useEffect(() => { - if (isOpen) { - fetchDiff(); - } - }, [isOpen, fetchDiff]); + fetchDiff(); + }, [fetchDiff]); const fetchExecutionState = useCallback( async (attemptId: string, taskId: string) => { if (!task) return; try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}` - ); + const result = await attemptsApi.getState(projectId, taskId, attemptId); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setExecutionState((prev) => { - if (JSON.stringify(prev) === JSON.stringify(result.data)) - return prev; - return result.data; - }); - } + if (result !== undefined) { + setExecutionState((prev) => { + if (JSON.stringify(prev) === JSON.stringify(result)) return prev; + return result; + }); } } catch (err) { console.error('Failed to fetch execution state:', err); @@ -167,20 +151,15 @@ const TaskDetailsProvider: FC<{ if (!task || !selectedAttempt) return; try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/open-editor`, - { - method: 'POST', - body: JSON.stringify( - editorType ? { editor_type: editorType } : null - ), - } + const result = await attemptsApi.openEditor( + projectId, + selectedAttempt.task_id, + selectedAttempt.id, + editorType ); - if (!response.ok) { - if (!editorType) { - setShowEditorDialog(true); - } + if (result === undefined && !editorType) { + setShowEditorDialog(true); } } catch (err) { console.error('Failed to open editor:', err); @@ -197,91 +176,56 @@ const TaskDetailsProvider: FC<{ if (!task) return; try { - const [activitiesResponse, processesResponse] = await Promise.all([ - makeRequest( - `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/activities` - ), - makeRequest( - `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/execution-processes` - ), + const [activitiesResult, processesResult] = await Promise.all([ + attemptsApi.getActivities(projectId, taskId, attemptId), + attemptsApi.getExecutionProcesses(projectId, taskId, attemptId), ]); - if (activitiesResponse.ok && processesResponse.ok) { - const activitiesResult: ApiResponse = - await activitiesResponse.json(); - const processesResult: ApiResponse = - await processesResponse.json(); + if (activitiesResult !== undefined && processesResult !== undefined) { + const runningActivities = activitiesResult.filter( + (activity) => + activity.status === 'setuprunning' || + activity.status === 'executorrunning' + ); - if ( - activitiesResult.success && - processesResult.success && - activitiesResult.data && - processesResult.data - ) { - const runningActivities = activitiesResult.data.filter( - (activity) => - activity.status === 'setuprunning' || - activity.status === 'executorrunning' + const runningProcessDetails: Record = {}; + + // Fetch details for running activities + for (const activity of runningActivities) { + const result = await executionProcessesApi.getDetails( + projectId, + activity.execution_process_id ); - const runningProcessDetails: Record = {}; - - // Fetch details for running activities - for (const activity of runningActivities) { - try { - const detailResponse = await makeRequest( - `/api/projects/${projectId}/execution-processes/${activity.execution_process_id}` - ); - if (detailResponse.ok) { - const detailResult: ApiResponse = - await detailResponse.json(); - if (detailResult.success && detailResult.data) { - runningProcessDetails[activity.execution_process_id] = - detailResult.data; - } - } - } catch (err) { - console.error( - `Failed to fetch execution process ${activity.execution_process_id}:`, - err - ); - } + if (result !== undefined) { + runningProcessDetails[activity.execution_process_id] = result; } - - // Also fetch setup script process details if it exists in the processes - const setupProcess = processesResult.data.find( - (process) => process.process_type === 'setupscript' - ); - if (setupProcess && !runningProcessDetails[setupProcess.id]) { - try { - const detailResponse = await makeRequest( - `/api/projects/${projectId}/execution-processes/${setupProcess.id}` - ); - if (detailResponse.ok) { - const detailResult: ApiResponse = - await detailResponse.json(); - if (detailResult.success && detailResult.data) { - runningProcessDetails[setupProcess.id] = detailResult.data; - } - } - } catch (err) { - console.error( - `Failed to fetch setup process details ${setupProcess.id}:`, - err - ); - } - } - - setAttemptData((prev) => { - const newData = { - activities: activitiesResult.data || [], - processes: processesResult.data || [], - runningProcessDetails, - }; - if (JSON.stringify(prev) === JSON.stringify(newData)) return prev; - return newData; - }); } + + // Also fetch setup script process details if it exists in the processes + const setupProcess = processesResult.find( + (process) => process.process_type === 'setupscript' + ); + if (setupProcess && !runningProcessDetails[setupProcess.id]) { + const result = await executionProcessesApi.getDetails( + projectId, + setupProcess.id + ); + + if (result !== undefined) { + runningProcessDetails[setupProcess.id] = result; + } + } + + setAttemptData((prev) => { + const newData = { + activities: activitiesResult, + processes: processesResult, + runningProcessDetails, + }; + if (JSON.stringify(prev) === JSON.stringify(newData)) return prev; + return newData; + }); } } catch (err) { console.error('Failed to fetch attempt data:', err); @@ -331,7 +275,7 @@ const TaskDetailsProvider: FC<{ // Refresh diff when coding agent is running and making changes useEffect(() => { - if (!executionState || !isOpen || !selectedAttempt) return; + if (!executionState || !selectedAttempt) return; const isCodingAgentRunning = executionState.execution_state === 'CodingAgentRunning'; @@ -349,11 +293,11 @@ const TaskDetailsProvider: FC<{ clearInterval(interval); }; } - }, [executionState, isOpen, selectedAttempt, fetchDiff]); + }, [executionState, selectedAttempt, fetchDiff]); // Refresh diff when coding agent completes or changes state useEffect(() => { - if (!executionState?.execution_state || !isOpen || !selectedAttempt) return; + if (!executionState?.execution_state || !selectedAttempt) return; const isCodingAgentComplete = executionState.execution_state === 'CodingAgentComplete'; @@ -376,7 +320,6 @@ const TaskDetailsProvider: FC<{ }, [ executionState?.execution_state, executionState?.has_changes, - isOpen, selectedAttempt, fetchDiff, activeTab, diff --git a/frontend/src/components/projects/project-detail.tsx b/frontend/src/components/projects/project-detail.tsx index b3de6c10..7c737a71 100644 --- a/frontend/src/components/projects/project-detail.tsx +++ b/frontend/src/components/projects/project-detail.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { @@ -10,18 +10,18 @@ import { } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { ProjectWithBranch, ApiResponse } from 'shared/types'; +import { ProjectWithBranch } from 'shared/types'; import { ProjectForm } from './project-form'; -import { makeRequest } from '@/lib/api'; +import { projectsApi } from '@/lib/api'; import { - ArrowLeft, - Edit, - Trash2, - Calendar, - Clock, AlertCircle, - Loader2, + ArrowLeft, + Calendar, CheckSquare, + Clock, + Edit, + Loader2, + Trash2, } from 'lucide-react'; interface ProjectDetailProps { @@ -39,22 +39,17 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { const fetchProject = useCallback(async () => { setLoading(true); setError(''); + try { - const response = await makeRequest( - `/api/projects/${projectId}/with-branch` - ); - const data: ApiResponse = await response.json(); - if (data.success && data.data) { - setProject(data.data); - } else { - setError('Project not found'); - } + const result = await projectsApi.getWithBranch(projectId); + setProject(result); } catch (error) { console.error('Failed to fetch project:', error); - setError('Failed to load project'); - } finally { - setLoading(false); + // @ts-expect-error it is type ApiError + setError(error.message || 'Failed to load project'); } + + setLoading(false); }, [projectId]); const handleDelete = async () => { @@ -67,15 +62,12 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { return; try { - const response = await makeRequest(`/api/projects/${projectId}`, { - method: 'DELETE', - }); - if (response.ok) { - onBack(); - } + await projectsApi.delete(projectId); + onBack(); } catch (error) { console.error('Failed to delete project:', error); - setError('Failed to delete project'); + // @ts-expect-error it is type ApiError + setError(error.message || 'Failed to delete project'); } }; diff --git a/frontend/src/components/projects/project-form.tsx b/frontend/src/components/projects/project-form.tsx index fb773785..64b071d9 100644 --- a/frontend/src/components/projects/project-form.tsx +++ b/frontend/src/components/projects/project-form.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -12,9 +12,9 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { FolderPicker } from '@/components/ui/folder-picker'; -import { Project, CreateProject, UpdateProject } from 'shared/types'; +import { CreateProject, Project, UpdateProject } from 'shared/types'; import { AlertCircle, Folder } from 'lucide-react'; -import { makeRequest } from '@/lib/api'; +import { projectsApi } from '@/lib/api'; interface ProjectFormProps { open: boolean; @@ -95,18 +95,12 @@ export function ProjectForm({ setup_script: setupScript.trim() || null, dev_script: devScript.trim() || null, }; - const response = await makeRequest(`/api/projects/${project.id}`, { - method: 'PUT', - body: JSON.stringify(updateData), - }); - if (!response.ok) { - throw new Error('Failed to update project'); - } - - const data = await response.json(); - if (!data.success) { - throw new Error(data.message || 'Failed to update project'); + try { + await projectsApi.update(project.id, updateData); + } catch (error) { + setError('Failed to update project'); + return; } } else { const createData: CreateProject = { @@ -116,18 +110,12 @@ export function ProjectForm({ setup_script: setupScript.trim() || null, dev_script: devScript.trim() || null, }; - const response = await makeRequest('/api/projects', { - method: 'POST', - body: JSON.stringify(createData), - }); - if (!response.ok) { - throw new Error('Failed to create project'); - } - - const data = await response.json(); - if (!data.success) { - throw new Error(data.message || 'Failed to create project'); + try { + await projectsApi.create(createData); + } catch (error) { + setError('Failed to create project'); + return; } } diff --git a/frontend/src/components/projects/project-list.tsx b/frontend/src/components/projects/project-list.tsx index 19005efa..065aa32a 100644 --- a/frontend/src/components/projects/project-list.tsx +++ b/frontend/src/components/projects/project-list.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { @@ -10,19 +10,19 @@ import { } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Project, ApiResponse } from 'shared/types'; +import { Project } from 'shared/types'; import { ProjectForm } from './project-form'; -import { makeRequest } from '@/lib/api'; +import { projectsApi } from '@/lib/api'; import { - Plus, - Edit, - Trash2, - Calendar, AlertCircle, - Loader2, - MoreHorizontal, + Calendar, + Edit, ExternalLink, FolderOpen, + Loader2, + MoreHorizontal, + Plus, + Trash2, } from 'lucide-react'; import { DropdownMenu, @@ -42,17 +42,13 @@ export function ProjectList() { const fetchProjects = async () => { setLoading(true); setError(''); + try { - const response = await makeRequest('/api/projects'); - const data: ApiResponse = await response.json(); - if (data.success && data.data) { - setProjects(data.data); - } else { - setError('Failed to load projects'); - } + const result = await projectsApi.getAll(); + setProjects(result); } catch (error) { console.error('Failed to fetch projects:', error); - setError('Failed to connect to server'); + setError('Failed to fetch projects'); } finally { setLoading(false); } @@ -67,12 +63,8 @@ export function ProjectList() { return; try { - const response = await makeRequest(`/api/projects/${id}`, { - method: 'DELETE', - }); - if (response.ok) { - fetchProjects(); - } + await projectsApi.delete(id); + fetchProjects(); } catch (error) { console.error('Failed to delete project:', error); setError('Failed to delete project'); @@ -86,20 +78,7 @@ export function ProjectList() { const handleOpenInIDE = async (projectId: string) => { try { - const response = await makeRequest( - `/api/projects/${projectId}/open-editor`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(null), - } - ); - - if (!response.ok) { - throw new Error('Failed to open project in IDE'); - } + await projectsApi.openEditor(projectId); } catch (error) { console.error('Failed to open project in IDE:', error); setError('Failed to open project in IDE'); diff --git a/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx b/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx index d1d2a62e..448d8e73 100644 --- a/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +++ b/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx @@ -7,9 +7,8 @@ import { DialogTitle, } from '@/components/ui/dialog.tsx'; import { Button } from '@/components/ui/button.tsx'; -import { makeRequest } from '@/lib/api.ts'; +import { attemptsApi } from '@/lib/api.ts'; import { useContext } from 'react'; -import { ApiResponse } from 'shared/types.ts'; import { TaskDeletingFilesContext, TaskDetailsContext, @@ -28,29 +27,19 @@ function DeleteFileConfirmationDialog() { if (!fileToDelete || !projectId || !task?.id || !selectedAttempt?.id) return; - try { - setDeletingFiles((prev) => new Set(prev).add(fileToDelete)); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/delete-file?file_path=${encodeURIComponent( - fileToDelete - )}`, - { - method: 'POST', - } - ); + setDeletingFiles((prev) => new Set(prev).add(fileToDelete)); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success) { - fetchDiff(); - } else { - setDiffError(result.message || 'Failed to delete file'); - } - } else { - setDiffError('Failed to delete file'); - } - } catch (err) { - setDiffError('Failed to delete file'); + try { + await attemptsApi.deleteFile( + projectId!, + selectedAttempt.task_id, + selectedAttempt.id, + fileToDelete + ); + await fetchDiff(); + } catch (error: unknown) { + // @ts-expect-error it is type ApiError + setDiffError(error.message || 'Failed to delete file'); } finally { setDeletingFiles((prev) => { const newSet = new Set(prev); diff --git a/frontend/src/components/tasks/TaskActivityHistory.tsx b/frontend/src/components/tasks/TaskActivityHistory.tsx index 3b76c151..c74dc99a 100644 --- a/frontend/src/components/tasks/TaskActivityHistory.tsx +++ b/frontend/src/components/tasks/TaskActivityHistory.tsx @@ -3,7 +3,7 @@ import { ChevronDown, ChevronUp, Clock, Code } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Chip } from '@/components/ui/chip'; -import { NormalizedConversationViewer } from './TaskDetails/NormalizedConversationViewer.tsx'; +import { NormalizedConversationViewer } from './TaskDetails/LogsTab/NormalizedConversationViewer.tsx'; import type { ExecutionProcess, TaskAttempt, diff --git a/frontend/src/components/tasks/TaskDetails/DiffCard.tsx b/frontend/src/components/tasks/TaskDetails/DiffCard.tsx index e8a863ed..5c0ad268 100644 --- a/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +++ b/frontend/src/components/tasks/TaskDetails/DiffCard.tsx @@ -4,6 +4,7 @@ import { GitCompare } from 'lucide-react'; import type { WorktreeDiff } from 'shared/types.ts'; import { TaskBackgroundRefreshContext } from '@/components/context/taskDetailsContext.ts'; import DiffFile from '@/components/tasks/TaskDetails/DiffFile.tsx'; +import { Loader } from '@/components/ui/loader'; interface DiffCardProps { diff: WorktreeDiff | null; @@ -57,10 +58,7 @@ export function DiffCard({
{isBackgroundRefreshing && (
-
- - Updating... - +
)}
diff --git a/frontend/src/components/tasks/TaskDetails/DiffTab.tsx b/frontend/src/components/tasks/TaskDetails/DiffTab.tsx index 4c6e2991..82dc99b1 100644 --- a/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/DiffTab.tsx @@ -1,6 +1,7 @@ import { DiffCard } from '@/components/tasks/TaskDetails/DiffCard.tsx'; import { useContext } from 'react'; import { TaskDiffContext } from '@/components/context/taskDetailsContext.ts'; +import { Loader } from '@/components/ui/loader'; function DiffTab() { const { diff, diffLoading, diffError } = useContext(TaskDiffContext); @@ -8,8 +9,7 @@ function DiffTab() { if (diffLoading) { return (
-
-

Loading changes...

+
); } diff --git a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx index 885a3ceb..d0102311 100644 --- a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx @@ -1,13 +1,15 @@ -import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { useContext } from 'react'; import { MessageSquare } from 'lucide-react'; -import { NormalizedConversationViewer } from '@/components/tasks/TaskDetails/NormalizedConversationViewer.tsx'; +import { NormalizedConversationViewer } from '@/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx'; import { TaskAttemptDataContext, TaskAttemptLoadingContext, TaskExecutionStateContext, TaskSelectedAttemptContext, } from '@/components/context/taskDetailsContext.ts'; -import Conversation from '@/components/tasks/TaskDetails/Conversation.tsx'; +import Conversation from '@/components/tasks/TaskDetails/LogsTab/Conversation.tsx'; +import { Loader } from '@/components/ui/loader'; +import SetupScriptRunning from '@/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx'; function LogsTab() { const { loading } = useContext(TaskAttemptLoadingContext); @@ -15,27 +17,10 @@ function LogsTab() { const { selectedAttempt } = useContext(TaskSelectedAttemptContext); const { attemptData } = useContext(TaskAttemptDataContext); - const [conversationUpdateTrigger, setConversationUpdateTrigger] = useState(0); - - const setupScrollRef = useRef(null); - - // Auto-scroll setup script logs to bottom - useEffect(() => { - if (setupScrollRef.current) { - setupScrollRef.current.scrollTop = setupScrollRef.current.scrollHeight; - } - }, [attemptData.runningProcessDetails]); - - // Callback to trigger auto-scroll when conversation updates - const handleConversationUpdate = useCallback(() => { - setConversationUpdateTrigger((prev) => prev + 1); - }, []); - if (loading) { return (
-
-

Loading...

+
); } @@ -80,30 +65,11 @@ function LogsTab() { // When setup script is running, show setup execution stdio if (isSetupRunning) { - // Find the setup script process in runningProcessDetails first, then fallback to processes - const setupProcess = executionState.setup_process_id - ? attemptData.runningProcessDetails[executionState.setup_process_id] - : Object.values(attemptData.runningProcessDetails).find( - (process) => process.process_type === 'setupscript' - ); - return ( -
-
-

Setup Script Running

-

- Preparing the environment for the coding agent... -

-
- - {setupProcess && ( -
- {[setupProcess.stdout || '', setupProcess.stderr || ''] - .filter(Boolean) - .join('\n') || 'Waiting for setup script output...'} -
- )} -
+ ); } @@ -127,10 +93,7 @@ function LogsTab() { {setupProcess && ( - + )} ); @@ -158,10 +121,7 @@ function LogsTab() { {codingAgentProcess && ( - + )} ); @@ -199,12 +159,7 @@ function LogsTab() { // When coding agent is running or complete, show conversation if (isCodingAgentRunning || isCodingAgentComplete || hasChanges) { - return ( - - ); + return ; } // Default case - unexpected state diff --git a/frontend/src/components/tasks/TaskDetails/Conversation.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx similarity index 85% rename from frontend/src/components/tasks/TaskDetails/Conversation.tsx rename to frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx index 4d9a1c40..ce898b95 100644 --- a/frontend/src/components/tasks/TaskDetails/Conversation.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx @@ -1,4 +1,4 @@ -import { NormalizedConversationViewer } from '@/components/tasks/TaskDetails/NormalizedConversationViewer.tsx'; +import { NormalizedConversationViewer } from '@/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx'; import { useCallback, useContext, @@ -8,21 +8,20 @@ import { useState, } from 'react'; import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext.ts'; +import { Loader } from '@/components/ui/loader.tsx'; -type Props = { - conversationUpdateTrigger: number; - handleConversationUpdate: () => void; -}; - -function Conversation({ - conversationUpdateTrigger, - handleConversationUpdate, -}: Props) { +function Conversation() { const { attemptData } = useContext(TaskAttemptDataContext); const [shouldAutoScrollLogs, setShouldAutoScrollLogs] = useState(true); + const [conversationUpdateTrigger, setConversationUpdateTrigger] = useState(0); const scrollContainerRef = useRef(null); + // Callback to trigger auto-scroll when conversation updates + const handleConversationUpdate = useCallback(() => { + setConversationUpdateTrigger((prev) => prev + 1); + }, []); + useEffect(() => { if (shouldAutoScrollLogs && scrollContainerRef.current) { scrollContainerRef.current.scrollTop = @@ -130,11 +129,17 @@ function Conversation({ ))} ) : ( -
-
-

Coding Agent Starting

-

Initializing conversation...

-
+ + Coding Agent Starting +
+ Initializing conversation... + + } + size={48} + className="py-8" + /> )} ); diff --git a/frontend/src/components/tasks/TaskDetails/NormalizedConversationViewer.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx similarity index 86% rename from frontend/src/components/tasks/TaskDetails/NormalizedConversationViewer.tsx rename to frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx index 8a27b080..aabcc601 100644 --- a/frontend/src/components/tasks/TaskDetails/NormalizedConversationViewer.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx @@ -1,9 +1,9 @@ import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Bot, Hammer, ToggleLeft, ToggleRight } from 'lucide-react'; -import { makeRequest } from '@/lib/api.ts'; +import { Loader } from '@/components/ui/loader.tsx'; +import { executionProcessesApi } from '@/lib/api.ts'; import { MarkdownRenderer } from '@/components/ui/markdown-renderer.tsx'; import type { - ApiResponse, ExecutionProcess, NormalizedConversation, NormalizedEntry, @@ -132,37 +132,22 @@ export function NormalizedConversationViewer({ setLoading(true); setError(null); } - - const response = await makeRequest( - `/api/projects/${projectId}/execution-processes/${executionProcess.id}/normalized-logs` + const result = await executionProcessesApi.getNormalizedLogs( + projectId, + executionProcess.id ); - - if (response.ok) { - const result: ApiResponse = - await response.json(); - if (result.success && result.data) { - setConversation((prev) => { - // Only update if content actually changed - if ( - !prev || - JSON.stringify(prev) !== JSON.stringify(result.data) - ) { - // Notify parent component of conversation update - if (onConversationUpdate) { - // Use setTimeout to ensure state update happens first - setTimeout(onConversationUpdate, 0); - } - return result.data; - } - return prev; - }); - } else if (!isPolling) { - setError(result.message || 'Failed to fetch normalized logs'); + setConversation((prev) => { + // Only update if content actually changed + if (!prev || JSON.stringify(prev) !== JSON.stringify(result)) { + // Notify parent component of conversation update + if (onConversationUpdate) { + // Use setTimeout to ensure state update happens first + setTimeout(onConversationUpdate, 0); + } + return result; } - } else if (!isPolling) { - const errorText = await response.text(); - setError(`Failed to fetch logs: ${errorText || response.statusText}`); - } + return prev; + }); } catch (err) { if (!isPolling) { setError( @@ -216,9 +201,7 @@ export function NormalizedConversationViewer({ if (loading) { return ( -
- Loading conversation... -
+ ); } @@ -299,7 +282,7 @@ export function NormalizedConversationViewer({
{displayEntries.map((entry, index) => ( ; +}; + +function SetupScriptRunning({ setupProcessId, runningProcessDetails }: Props) { + const setupScrollRef = useRef(null); + + // Auto-scroll setup script logs to bottom + useEffect(() => { + if (setupScrollRef.current) { + setupScrollRef.current.scrollTop = setupScrollRef.current.scrollHeight; + } + }, [runningProcessDetails]); + + const setupProcess = useMemo( + () => + setupProcessId + ? runningProcessDetails[setupProcessId] + : Object.values(runningProcessDetails).find( + (process) => process.process_type === 'setupscript' + ), + [setupProcessId, runningProcessDetails] + ); + + return ( +
+
+

Setup Script Running

+

+ Preparing the environment for the coding agent... +

+
+ + {setupProcess && ( +
+ {[setupProcess.stdout || '', setupProcess.stderr || ''] + .filter(Boolean) + .join('\n') || 'Waiting for setup script output...'} +
+ )} +
+ ); +} + +export default SetupScriptRunning; diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index 1bbab33e..320fdc84 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -18,7 +18,6 @@ interface TaskDetailsPanelProps { task: TaskWithAttemptStatus | null; projectHasDevScript?: boolean; projectId: string; - isOpen: boolean; onClose: () => void; onEditTask?: (task: TaskWithAttemptStatus) => void; onDeleteTask?: (taskId: string) => void; @@ -29,7 +28,6 @@ export function TaskDetailsPanel({ task, projectHasDevScript, projectId, - isOpen, onClose, onEditTask, onDeleteTask, @@ -51,7 +49,7 @@ export function TaskDetailsPanel({ // Handle ESC key locally to prevent global navigation useEffect(() => { - if (!isOpen || isDialogOpen) return; + if (isDialogOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { @@ -63,18 +61,17 @@ export function TaskDetailsPanel({ document.addEventListener('keydown', handleKeyDown, true); return () => document.removeEventListener('keydown', handleKeyDown, true); - }, [isOpen, onClose, isDialogOpen]); + }, [onClose, isDialogOpen]); return ( <> - {!task || !isOpen ? null : ( + {!task ? null : ( diff --git a/frontend/src/components/tasks/TaskDetailsToolbar.tsx b/frontend/src/components/tasks/TaskDetailsToolbar.tsx index 37ed2465..7bd43400 100644 --- a/frontend/src/components/tasks/TaskDetailsToolbar.tsx +++ b/frontend/src/components/tasks/TaskDetailsToolbar.tsx @@ -2,8 +2,8 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import { Play } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useConfig } from '@/components/config-provider'; -import { makeRequest } from '@/lib/api'; -import type { ApiResponse, GitBranch, TaskAttempt } from 'shared/types'; +import { attemptsApi, projectsApi } from '@/lib/api'; +import type { GitBranch, TaskAttempt } from 'shared/types'; import { TaskAttemptDataContext, TaskAttemptLoadingContext, @@ -61,21 +61,13 @@ function TaskDetailsToolbar() { const [error, setError] = useState(null); const fetchProjectBranches = useCallback(async () => { - try { - const response = await makeRequest(`/api/projects/${projectId}/branches`); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setBranches(result.data); - // Set current branch as default - const currentBranch = result.data.find((b) => b.is_current); - if (currentBranch && !selectedBranch) { - setSelectedBranch(currentBranch.name); - } - } - } - } catch (err) { - console.error('Failed to fetch project branches:', err); + const result = await projectsApi.getBranches(projectId); + + setBranches(result); + // Set current branch as default + const currentBranch = result.find((b) => b.is_current); + if (currentBranch && !selectedBranch) { + setSelectedBranch(currentBranch.name); } }, [projectId, selectedBranch]); @@ -127,44 +119,36 @@ function TaskDetailsToolbar() { try { setLoading(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${task.id}/attempts` - ); + const result = await attemptsApi.getAll(projectId, task.id); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setTaskAttempts((prev) => { - if (JSON.stringify(prev) === JSON.stringify(result.data)) - return prev; - return result.data || prev; - }); + setTaskAttempts((prev) => { + if (JSON.stringify(prev) === JSON.stringify(result)) return prev; + return result || prev; + }); - if (result.data.length > 0) { - const latestAttempt = result.data.reduce((latest, current) => - new Date(current.created_at) > new Date(latest.created_at) - ? current - : latest - ); - setSelectedAttempt((prev) => { - if (JSON.stringify(prev) === JSON.stringify(latestAttempt)) - return prev; - return latestAttempt; - }); - fetchAttemptData(latestAttempt.id, latestAttempt.task_id); - fetchExecutionState(latestAttempt.id, latestAttempt.task_id); - } else { - setSelectedAttempt(null); - setAttemptData({ - activities: [], - processes: [], - runningProcessDetails: {}, - }); - } - } + if (result.length > 0) { + const latestAttempt = result.reduce((latest, current) => + new Date(current.created_at) > new Date(latest.created_at) + ? current + : latest + ); + setSelectedAttempt((prev) => { + if (JSON.stringify(prev) === JSON.stringify(latestAttempt)) + return prev; + return latestAttempt; + }); + fetchAttemptData(latestAttempt.id, latestAttempt.task_id); + fetchExecutionState(latestAttempt.id, latestAttempt.task_id); + } else { + setSelectedAttempt(null); + setAttemptData({ + activities: [], + processes: [], + runningProcessDetails: {}, + }); } - } catch (err) { - console.error('Failed to fetch task attempts:', err); + } catch (error) { + // we already logged error } finally { setLoading(false); } diff --git a/frontend/src/components/tasks/TaskFollowUpSection.tsx b/frontend/src/components/tasks/TaskFollowUpSection.tsx index 21103431..0624f614 100644 --- a/frontend/src/components/tasks/TaskFollowUpSection.tsx +++ b/frontend/src/components/tasks/TaskFollowUpSection.tsx @@ -3,12 +3,13 @@ import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { FileSearchTextarea } from '@/components/ui/file-search-textarea'; import { useContext, useMemo, useState } from 'react'; -import { makeRequest } from '@/lib/api.ts'; +import { attemptsApi } from '@/lib/api.ts'; import { TaskAttemptDataContext, TaskDetailsContext, TaskSelectedAttemptContext, } from '@/components/context/taskDetailsContext.ts'; +import { Loader } from '@/components/ui/loader'; export function TaskFollowUpSection() { const { task, projectId } = useContext(TaskDetailsContext); @@ -49,36 +50,19 @@ export function TaskFollowUpSection() { try { setIsSendingFollowUp(true); setFollowUpError(null); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/follow-up`, + await attemptsApi.followUp( + projectId!, + selectedAttempt.task_id, + selectedAttempt.id, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prompt: followUpMessage.trim(), - }), + prompt: followUpMessage.trim(), } ); - - if (response.ok) { - setFollowUpMessage(''); - fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); - } else { - const errorText = await response.text(); - setFollowUpError( - `Failed to start follow-up execution: ${ - errorText || response.statusText - }` - ); - } - } catch (err) { - setFollowUpError( - `Failed to send follow-up: ${ - err instanceof Error ? err.message : 'Unknown error' - }` - ); + setFollowUpMessage(''); + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + } catch (error: unknown) { + // @ts-expect-error it is type ApiError + setFollowUpError(`Failed to start follow-up execution: ${error.message}`); } finally { setIsSendingFollowUp(false); } @@ -127,7 +111,7 @@ export function TaskFollowUpSection() { size="sm" > {isSendingFollowUp ? ( -
+ ) : ( <> diff --git a/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx b/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx index 8738a0fb..0079d8bb 100644 --- a/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +++ b/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx @@ -17,7 +17,7 @@ import { } from '@/components/ui/dropdown-menu.tsx'; import { Input } from '@/components/ui/input.tsx'; import type { GitBranch, TaskAttempt } from 'shared/types.ts'; -import { makeRequest } from '@/lib/api.ts'; +import { attemptsApi } from '@/lib/api.ts'; import { TaskAttemptDataContext, TaskDetailsContext, @@ -72,22 +72,13 @@ function CreateAttempt({ const onCreateNewAttempt = async (executor?: string, baseBranch?: string) => { try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${task.id}/attempts`, - { - method: 'POST', - body: JSON.stringify({ - executor: executor || selectedExecutor, - base_branch: baseBranch || selectedBranch, - }), - } - ); - - if (response.ok) { - fetchTaskAttempts(); - } - } catch (err) { - console.error('Failed to create new attempt:', err); + await attemptsApi.create(projectId!, task.id, { + executor: executor || selectedExecutor, + base_branch: baseBranch || selectedBranch, + }); + fetchTaskAttempts(); + } catch (error) { + // Optionally handle error } }; diff --git a/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx b/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx index 2c3c141b..e664ad7f 100644 --- a/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +++ b/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx @@ -22,9 +22,9 @@ import { TaskDetailsContext, TaskSelectedAttemptContext, } from '@/components/context/taskDetailsContext.ts'; -import { makeRequest } from '@/lib/api.ts'; +import { ApiError, attemptsApi } from '@/lib/api.ts'; import { ProvidePatDialog } from '@/components/ProvidePatDialog'; -import { ApiResponse, GitBranch } from 'shared/types.ts'; +import { GitBranch } from 'shared/types.ts'; type Props = { showCreatePRDialog: boolean; @@ -71,59 +71,51 @@ function CreatePrDialog({ const handleConfirmCreatePR = useCallback(async () => { if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return; + setCreatingPR(true); + try { - setCreatingPR(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/create-pr`, + const prUrl = await attemptsApi.createPR( + projectId!, + selectedAttempt.task_id, + selectedAttempt.id, { - method: 'POST', - body: JSON.stringify({ - title: prTitle, - body: prBody || null, - base_branch: prBaseBranch || null, - }), + title: prTitle, + body: prBody || null, + base_branch: prBaseBranch || null, } ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - console.log(result); - if (result.success && result.data) { - // Open the PR URL in a new tab - window.open(result.data, '_blank'); - setShowCreatePRDialog(false); - // Reset form - setPrTitle(''); - setPrBody(''); - setPrBaseBranch(selectedAttempt?.base_branch || 'main'); - } else if (result.message === 'insufficient_github_permissions') { - setShowCreatePRDialog(false); - setPatDialogError(null); - setShowPatDialog(true); - } else if (result.message === 'github_repo_not_found_or_no_access') { - setShowCreatePRDialog(false); - setPatDialogError( - 'Your token does not have access to this repository, or the repository does not exist. Please check the repository URL and/or provide a Personal Access Token with access.' - ); - setShowPatDialog(true); - } else { - setError(result.message || 'Failed to create GitHub PR'); - } - } else if (response.status === 403) { + // Open the PR URL in a new tab + window.open(prUrl, '_blank'); + setShowCreatePRDialog(false); + // Reset form + setPrTitle(''); + setPrBody(''); + setPrBaseBranch(selectedAttempt?.base_branch || 'main'); + } catch (err) { + const error = err as ApiError; + if (error.message === 'insufficient_github_permissions') { setShowCreatePRDialog(false); setPatDialogError(null); setShowPatDialog(true); - } else if (response.status === 404) { + } else if (error.message === 'github_repo_not_found_or_no_access') { + setShowCreatePRDialog(false); + setPatDialogError( + 'Your token does not have access to this repository, or the repository does not exist. Please check the repository URL and/or provide a Personal Access Token with access.' + ); + setShowPatDialog(true); + } else if (error.status === 403) { + setShowCreatePRDialog(false); + setPatDialogError(null); + setShowPatDialog(true); + } else if (error.status === 404) { setShowCreatePRDialog(false); setPatDialogError( 'Your token does not have access to this repository, or the repository does not exist. Please check the repository URL and/or provide a Personal Access Token with access.' ); setShowPatDialog(true); } else { - setError('Failed to create GitHub PR'); + setError(error.message || 'Failed to create GitHub PR'); } - } catch (err) { - setError('Failed to create GitHub PR'); } finally { setCreatingPR(false); } @@ -136,6 +128,8 @@ function CreatePrDialog({ setCreatingPR, setError, setShowCreatePRDialog, + setPatDialogError, + setShowPatDialog, ]); const handleCancelCreatePR = useCallback(() => { diff --git a/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx b/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx index e869b109..2a4563ed 100644 --- a/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +++ b/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx @@ -21,7 +21,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu.tsx'; -import { makeRequest } from '@/lib/api.ts'; +import { attemptsApi, executionProcessesApi } from '@/lib/api.ts'; import { Dispatch, SetStateAction, @@ -32,7 +32,6 @@ import { useState, } from 'react'; import type { - ApiResponse, BranchStatus, ExecutionProcess, TaskAttempt, @@ -132,15 +131,11 @@ function CurrentAttempt({ if (!runningDevServer || !task || !selectedAttempt) return; try { - const response = await makeRequest( - `/api/projects/${projectId}/execution-processes/${runningDevServer.id}` + const result = await executionProcessesApi.getDetails( + projectId, + runningDevServer.id ); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setDevServerDetails(result.data); - } - } + setDevServerDetails(result); } catch (err) { console.error('Failed to fetch dev server details:', err); } @@ -163,23 +158,11 @@ function CurrentAttempt({ setIsStartingDevServer(true); try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/start-dev-server`, - { - method: 'POST', - } + await attemptsApi.startDevServer( + projectId, + selectedAttempt.task_id, + selectedAttempt.id ); - - if (!response.ok) { - throw new Error('Failed to start dev server'); - } - - const data: ApiResponse = await response.json(); - - if (!data.success) { - throw new Error(data.message || 'Failed to start dev server'); - } - fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); } catch (err) { console.error('Failed to start dev server:', err); @@ -194,17 +177,12 @@ function CurrentAttempt({ setIsStartingDevServer(true); try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/execution-processes/${runningDevServer.id}/stop`, - { - method: 'POST', - } + await attemptsApi.stopExecutionProcess( + projectId, + selectedAttempt.task_id, + selectedAttempt.id, + runningDevServer.id ); - - if (!response.ok) { - throw new Error('Failed to stop dev server'); - } - fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); } catch (err) { console.error('Failed to stop dev server:', err); @@ -218,19 +196,15 @@ function CurrentAttempt({ try { setIsStopping(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/stop`, - { - method: 'POST', - } + await attemptsApi.stop( + projectId, + selectedAttempt.task_id, + selectedAttempt.id ); - - if (response.ok) { - await fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); - setTimeout(() => { - fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); - }, 1000); - } + await fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + setTimeout(() => { + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + }, 1000); } catch (err) { console.error('Failed to stop executions:', err); } finally { @@ -259,30 +233,21 @@ function CurrentAttempt({ try { setBranchStatusLoading(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/branch-status` + const result = await attemptsApi.getBranchStatus( + projectId, + selectedAttempt.task_id, + selectedAttempt.id ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setBranchStatus((prev) => { - if (JSON.stringify(prev) === JSON.stringify(result.data)) - return prev; - return result.data; - }); - } else { - setError('Failed to load branch status'); - } - } else { - setError('Failed to load branch status'); - } + setBranchStatus((prev) => { + if (JSON.stringify(prev) === JSON.stringify(result)) return prev; + return result; + }); } catch (err) { setError('Failed to load branch status'); } finally { setBranchStatusLoading(false); } - }, [projectId, selectedAttempt?.id, selectedAttempt?.task_id]); + }, [projectId, selectedAttempt?.id, selectedAttempt?.task_id, setError]); // Fetch branch status when selected attempt changes useEffect(() => { @@ -296,26 +261,17 @@ function CurrentAttempt({ try { setMerging(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/merge`, - { - method: 'POST', - } + await attemptsApi.merge( + projectId, + selectedAttempt.task_id, + selectedAttempt.id ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success) { - // Refetch branch status to show updated state - fetchBranchStatus(); - } else { - setError(result.message || 'Failed to merge changes'); - } - } else { - setError('Failed to merge changes'); - } - } catch (err) { - setError('Failed to merge changes'); + // Refetch branch status to show updated state + fetchBranchStatus(); + } catch (error) { + console.error('Failed to merge changes:', error); + // @ts-expect-error it is type ApiError + setError(error.message || 'Failed to merge changes'); } finally { setMerging(false); } @@ -326,24 +282,13 @@ function CurrentAttempt({ try { setRebasing(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/rebase`, - { - method: 'POST', - } + await attemptsApi.rebase( + projectId, + selectedAttempt.task_id, + selectedAttempt.id ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success) { - // Refresh branch status after rebase - fetchBranchStatus(); - } else { - setError(result.message || 'Failed to rebase branch'); - } - } else { - setError('Failed to rebase branch'); - } + // Refresh branch status after rebase + fetchBranchStatus(); } catch (err) { setError('Failed to rebase branch'); } finally { diff --git a/frontend/src/components/ui/file-search-textarea.tsx b/frontend/src/components/ui/file-search-textarea.tsx index a2bba11b..185058c4 100644 --- a/frontend/src/components/ui/file-search-textarea.tsx +++ b/frontend/src/components/ui/file-search-textarea.tsx @@ -1,8 +1,7 @@ import { KeyboardEvent, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Textarea } from '@/components/ui/textarea'; -import { makeRequest } from '@/lib/api'; -import { ApiResponse } from 'shared/types.ts'; +import { projectsApi } from '@/lib/api'; interface FileSearchResult { path: string; @@ -51,19 +50,12 @@ export function FileSearchTextarea({ const searchFiles = async () => { setIsLoading(true); - try { - const response = await makeRequest( - `/api/projects/${projectId}/search?q=${encodeURIComponent(searchQuery)}` - ); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setSearchResults(result.data); - setShowDropdown(true); - setSelectedIndex(-1); - } - } + try { + const result = await projectsApi.searchFiles(projectId, searchQuery); + setSearchResults(result); + setShowDropdown(true); + setSelectedIndex(-1); } catch (error) { console.error('Failed to search files:', error); } finally { diff --git a/frontend/src/components/ui/folder-picker.tsx b/frontend/src/components/ui/folder-picker.tsx index b419fe34..7f8ff604 100644 --- a/frontend/src/components/ui/folder-picker.tsx +++ b/frontend/src/components/ui/folder-picker.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -11,15 +11,15 @@ import { } from '@/components/ui/dialog'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { + AlertCircle, + ChevronUp, + File, Folder, FolderOpen, - File, - AlertCircle, Home, - ChevronUp, Search, } from 'lucide-react'; -import { makeRequest } from '@/lib/api'; +import { fileSystemApi } from '@/lib/api'; import { DirectoryEntry } from 'shared/types'; interface FolderPickerProps { @@ -65,25 +65,13 @@ export function FolderPicker({ setError(''); try { - const queryParam = path ? `?path=${encodeURIComponent(path)}` : ''; - const response = await makeRequest(`/api/filesystem/list${queryParam}`); - - if (!response.ok) { - throw new Error('Failed to load directory'); - } - - const data = await response.json(); - - if (data.success) { - setEntries(data.data || []); - const newPath = path || data.message || ''; - setCurrentPath(newPath); - // Update manual path if we have a specific path (not for initial home directory load) - if (path) { - setManualPath(newPath); - } - } else { - setError(data.message || 'Failed to load directory'); + const result = await fileSystemApi.list(path); + setEntries(result.entries || []); + const newPath = result.current_path || ''; + setCurrentPath(newPath); + // Update manual path if we have a specific path (not for initial home directory load) + if (path) { + setManualPath(newPath); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load directory'); diff --git a/frontend/src/components/ui/loader.tsx b/frontend/src/components/ui/loader.tsx new file mode 100644 index 00000000..49107325 --- /dev/null +++ b/frontend/src/components/ui/loader.tsx @@ -0,0 +1,24 @@ +import { Loader2 } from 'lucide-react'; +import React from 'react'; + +interface LoaderProps { + message?: string | React.ReactElement; + size?: number; + className?: string; +} + +export const Loader: React.FC = ({ + message, + size = 32, + className = '', +}) => ( +
+ + {!!message && ( +
{message}
+ )} +
+); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 50d1188b..6d3f3e0b 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,3 +1,31 @@ +// Import all necessary types from shared types +import { + BranchStatus, + Config, + CreateFollowUpAttempt, + CreateProject, + CreateTask, + CreateTaskAndStart, + CreateTaskAttempt, + DirectoryEntry, + type EditorType, + ExecutionProcess, + ExecutionProcessSummary, + GitBranch, + NormalizedConversation, + Project, + ProjectWithBranch, + StartGitHubDeviceFlowType, + Task, + TaskAttempt, + TaskAttemptActivityWithPrompt, + TaskAttemptState, + TaskWithAttemptStatus, + UpdateProject, + UpdateTask, + WorktreeDiff, +} from 'shared/types'; + export const makeRequest = async (url: string, options: RequestInit = {}) => { const headers = { 'Content-Type': 'application/json', @@ -9,3 +37,521 @@ export const makeRequest = async (url: string, options: RequestInit = {}) => { headers, }); }; + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; +} + +// Additional interface for file search results +export interface FileSearchResult { + path: string; + name: string; +} + +// Directory listing response +export interface DirectoryListResponse { + entries: DirectoryEntry[]; + current_path: string; +} + +export class ApiError extends Error { + constructor( + message: string, + public status?: number, + public response?: Response + ) { + super(message); + this.name = 'ApiError'; + } +} + +const handleApiResponse = async (response: Response): Promise => { + if (!response.ok) { + let errorMessage = `Request failed with status ${response.status}`; + + try { + const errorData = await response.json(); + if (errorData.message) { + errorMessage = errorData.message; + } + } catch { + // Fallback to status text if JSON parsing fails + errorMessage = response.statusText || errorMessage; + } + + console.error('[API Error]', { + message: errorMessage, + status: response.status, + response, + endpoint: response.url, + timestamp: new Date().toISOString(), + }); + throw new ApiError(errorMessage, response.status, response); + } + + const result: ApiResponse = await response.json(); + + if (!result.success) { + console.error('[API Error]', { + message: result.message || 'API request failed', + status: response.status, + response, + endpoint: response.url, + timestamp: new Date().toISOString(), + }); + throw new ApiError(result.message || 'API request failed'); + } + + return result.data as T; +}; + +// Project Management APIs +export const projectsApi = { + getAll: async (): Promise => { + const response = await makeRequest('/api/projects'); + return handleApiResponse(response); + }, + + getById: async (id: string): Promise => { + const response = await makeRequest(`/api/projects/${id}`); + return handleApiResponse(response); + }, + + getWithBranch: async (id: string): Promise => { + const response = await makeRequest(`/api/projects/${id}/with-branch`); + return handleApiResponse(response); + }, + + create: async (data: CreateProject): Promise => { + const response = await makeRequest('/api/projects', { + method: 'POST', + body: JSON.stringify(data), + }); + return handleApiResponse(response); + }, + + update: async (id: string, data: UpdateProject): Promise => { + const response = await makeRequest(`/api/projects/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + return handleApiResponse(response); + }, + + delete: async (id: string): Promise => { + const response = await makeRequest(`/api/projects/${id}`, { + method: 'DELETE', + }); + return handleApiResponse(response); + }, + + openEditor: async (id: string): Promise => { + const response = await makeRequest(`/api/projects/${id}/open-editor`, { + method: 'POST', + }); + return handleApiResponse(response); + }, + + getBranches: async (id: string): Promise => { + const response = await makeRequest(`/api/projects/${id}/branches`); + return handleApiResponse(response); + }, + + searchFiles: async ( + id: string, + query: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${id}/search?q=${encodeURIComponent(query)}` + ); + return handleApiResponse(response); + }, +}; + +// Task Management APIs +export const tasksApi = { + getAll: async (projectId: string): Promise => { + const response = await makeRequest(`/api/projects/${projectId}/tasks`); + return handleApiResponse(response); + }, + + create: async (projectId: string, data: CreateTask): Promise => { + const response = await makeRequest(`/api/projects/${projectId}/tasks`, { + method: 'POST', + body: JSON.stringify(data), + }); + return handleApiResponse(response); + }, + + createAndStart: async ( + projectId: string, + data: CreateTaskAndStart + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/create-and-start`, + { + method: 'POST', + body: JSON.stringify(data), + } + ); + return handleApiResponse(response); + }, + + update: async ( + projectId: string, + taskId: string, + data: UpdateTask + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}`, + { + method: 'PUT', + body: JSON.stringify(data), + } + ); + return handleApiResponse(response); + }, + + delete: async (projectId: string, taskId: string): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}`, + { + method: 'DELETE', + } + ); + return handleApiResponse(response); + }, +}; + +// Task Attempts APIs +export const attemptsApi = { + getAll: async (projectId: string, taskId: string): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts` + ); + return handleApiResponse(response); + }, + + create: async ( + projectId: string, + taskId: string, + data: CreateTaskAttempt + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts`, + { + method: 'POST', + body: JSON.stringify(data), + } + ); + return handleApiResponse(response); + }, + + getState: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}` + ); + return handleApiResponse(response); + }, + + stop: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/stop`, + { + method: 'POST', + } + ); + return handleApiResponse(response); + }, + + followUp: async ( + projectId: string, + taskId: string, + attemptId: string, + data: CreateFollowUpAttempt + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/follow-up`, + { + method: 'POST', + body: JSON.stringify(data), + } + ); + return handleApiResponse(response); + }, + + getActivities: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/activities` + ); + return handleApiResponse(response); + }, + + getDiff: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/diff` + ); + return handleApiResponse(response); + }, + + deleteFile: async ( + projectId: string, + taskId: string, + attemptId: string, + fileToDelete: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/delete-filefile_path=${encodeURIComponent( + fileToDelete + )}`, + { + method: 'POST', + } + ); + return handleApiResponse(response); + }, + + openEditor: async ( + projectId: string, + taskId: string, + attemptId: string, + editorType?: EditorType + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/open-editor`, + { + method: 'POST', + body: JSON.stringify(editorType ? { editor_type: editorType } : null), + } + ); + return handleApiResponse(response); + }, + + getBranchStatus: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/branch-status` + ); + return handleApiResponse(response); + }, + + merge: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/merge`, + { + method: 'POST', + } + ); + return handleApiResponse(response); + }, + + rebase: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/rebase`, + { + method: 'POST', + } + ); + return handleApiResponse(response); + }, + + createPR: async ( + projectId: string, + taskId: string, + attemptId: string, + data: { + title: string; + body: string | null; + base_branch: string | null; + } + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/create-pr`, + { + method: 'POST', + body: JSON.stringify(data), + } + ); + return handleApiResponse(response); + }, + + startDevServer: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/start-dev-server`, + { + method: 'POST', + } + ); + return handleApiResponse(response); + }, + + getExecutionProcesses: async ( + projectId: string, + taskId: string, + attemptId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/execution-processes` + ); + return handleApiResponse(response); + }, + + stopExecutionProcess: async ( + projectId: string, + taskId: string, + attemptId: string, + processId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/execution-processes/${processId}/stop`, + { + method: 'POST', + } + ); + return handleApiResponse(response); + }, +}; + +// Execution Process APIs +export const executionProcessesApi = { + getDetails: async ( + projectId: string, + processId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/execution-processes/${processId}` + ); + return handleApiResponse(response); + }, + + getNormalizedLogs: async ( + projectId: string, + processId: string + ): Promise => { + const response = await makeRequest( + `/api/projects/${projectId}/execution-processes/${processId}/normalized-logs` + ); + return handleApiResponse(response); + }, +}; + +// File System APIs +export const fileSystemApi = { + list: async (path?: string): Promise => { + const queryParam = path ? `?path=${encodeURIComponent(path)}` : ''; + const response = await makeRequest(`/api/filesystem/list${queryParam}`); + return handleApiResponse(response); + }, +}; + +// Config APIs +export const configApi = { + getConfig: async (): Promise => { + const response = await makeRequest('/api/config'); + return handleApiResponse(response); + }, + saveConfig: async (config: Config): Promise => { + const response = await makeRequest('/api/config', { + method: 'POST', + body: JSON.stringify(config), + }); + return handleApiResponse(response); + }, +}; + +// GitHub Device Auth APIs +export const githubAuthApi = { + checkGithubToken: async (): Promise => { + try { + const response = await makeRequest('/api/auth/github/check'); + const result: ApiResponse = await response.json(); + if (!result.success && result.message === 'github_token_invalid') { + return false; + } + return result.success; + } catch (err) { + // On network/server error, return undefined (unknown) + return undefined; + } + }, + start: async (): Promise => { + const response = await makeRequest('/api/auth/github/device/start', { + method: 'POST', + }); + return handleApiResponse(response); + }, + poll: async (device_code: string): Promise => { + const response = await makeRequest('/api/auth/github/device/poll', { + method: 'POST', + body: JSON.stringify({ device_code }), + headers: { 'Content-Type': 'application/json' }, + }); + return handleApiResponse(response); + }, +}; + +// MCP Servers APIs +export const mcpServersApi = { + load: async (executor: string): Promise => { + const response = await makeRequest( + `/api/mcp-servers?executor=${encodeURIComponent(executor)}` + ); + return handleApiResponse(response); + }, + save: async (executor: string, serversConfig: any): Promise => { + const response = await makeRequest( + `/api/mcp-servers?executor=${encodeURIComponent(executor)}`, + { + method: 'POST', + body: JSON.stringify(serversConfig), + } + ); + if (!response.ok) { + const errorData = await response.json(); + console.error('[API Error] Failed to save MCP servers', { + message: errorData.message, + status: response.status, + response, + timestamp: new Date().toISOString(), + }); + throw new ApiError( + errorData.message || 'Failed to save MCP servers', + response.status, + response + ); + } + }, +}; diff --git a/frontend/src/pages/McpServers.tsx b/frontend/src/pages/McpServers.tsx index 3b46d1cf..34d9a240 100644 --- a/frontend/src/pages/McpServers.tsx +++ b/frontend/src/pages/McpServers.tsx @@ -20,6 +20,7 @@ import { Textarea } from '@/components/ui/textarea'; import { Loader2 } from 'lucide-react'; import { EXECUTOR_TYPES, EXECUTOR_LABELS } from 'shared/types'; import { useConfig } from '@/components/config-provider'; +import { mcpServersApi } from '../lib/api'; export function McpServers() { const { config } = useConfig(); @@ -55,45 +56,31 @@ export function McpServers() { try { // Load MCP servers for the selected executor - const response = await fetch( - `/api/mcp-servers?executor=${executorType}` - ); - if (response.ok) { - const result = await response.json(); - if (result.success) { - // Handle new response format with servers and config_path - const data = result.data || {}; - const servers = data.servers || {}; - const configPath = data.config_path || ''; + const result = await mcpServersApi.load(executorType); + // Handle new response format with servers and config_path + const data = result || {}; + const servers = data.servers || {}; + const configPath = data.config_path || ''; - // Create the full configuration structure based on executor type - let fullConfig; - if (executorType === 'amp') { - // For AMP, use the amp.mcpServers structure - fullConfig = { 'amp.mcpServers': servers }; - } else { - // For other executors, use the standard mcpServers structure - fullConfig = { mcpServers: servers }; - } - - const configJson = JSON.stringify(fullConfig, null, 2); - setMcpServers(configJson); - setMcpConfigPath(configPath); - } + // Create the full configuration structure based on executor type + let fullConfig; + if (executorType === 'amp') { + // For AMP, use the amp.mcpServers structure + fullConfig = { 'amp.mcpServers': servers }; } else { - const result = await response.json(); - if ( - result.message && - result.message.includes('does not support MCP') - ) { - // This executor doesn't support MCP - show warning message - setMcpError(result.message); - } else { - console.warn('Failed to load MCP servers:', response.statusText); - } + // For other executors, use the standard mcpServers structure + fullConfig = { mcpServers: servers }; + } + + const configJson = JSON.stringify(fullConfig, null, 2); + setMcpServers(configJson); + setMcpConfigPath(configPath); + } catch (err: any) { + if (err?.message && err.message.includes('does not support MCP')) { + setMcpError(err.message); + } else { + console.error('Error loading MCP servers:', err); } - } catch (err) { - console.error('Error loading MCP servers:', err); } finally { setMcpLoading(false); } @@ -215,21 +202,7 @@ export function McpServers() { mcpServersConfig = fullConfig.mcpServers; } - const mcpResponse = await fetch( - `/api/mcp-servers?executor=${selectedMcpExecutor}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(mcpServersConfig), - } - ); - - if (!mcpResponse.ok) { - const errorData = await mcpResponse.json(); - throw new Error(errorData.message || 'Failed to save MCP servers'); - } + await mcpServersApi.save(selectedMcpExecutor, mcpServersConfig); // Show success feedback setSuccess(true); diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index d33387ff..ecac725b 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -4,7 +4,8 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { FolderOpen, Plus, Settings } from 'lucide-react'; -import { makeRequest } from '@/lib/api'; +import { Loader } from '@/components/ui/loader'; +import { projectsApi, tasksApi } from '@/lib/api'; import { TaskFormDialog } from '@/components/tasks/TaskFormDialog'; import { ProjectForm } from '@/components/projects/project-form'; import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts'; @@ -16,7 +17,6 @@ import { import { TaskKanbanBoard } from '@/components/tasks/TaskKanbanBoard'; import { TaskDetailsPanel } from '@/components/tasks/TaskDetailsPanel'; import type { - ApiResponse, CreateTaskAndStart, ExecutorConfig, ProjectWithBranch, @@ -56,20 +56,7 @@ export function ProjectTasks() { if (!projectId) return; try { - const response = await makeRequest( - `/api/projects/${projectId}/open-editor`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(null), - } - ); - - if (!response.ok) { - throw new Error('Failed to open project in IDE'); - } + await projectsApi.openEditor(projectId); } catch (error) { console.error('Failed to open project in IDE:', error); setError('Failed to open project in IDE'); @@ -116,19 +103,8 @@ export function ProjectTasks() { const fetchProject = useCallback(async () => { try { - const response = await makeRequest( - `/api/projects/${projectId}/with-branch` - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setProject(result.data); - } - } else if (response.status === 404) { - setError('Project not found'); - navigate('/projects'); - } + const result = await projectsApi.getWithBranch(projectId!); + setProject(result); } catch (err) { setError('Failed to load project'); } @@ -140,38 +116,28 @@ export function ProjectTasks() { if (!skipLoading) { setLoading(true); } - const response = await makeRequest(`/api/projects/${projectId}/tasks`); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - // Only update if data has actually changed - setTasks((prevTasks) => { - const newTasks = result.data!; - if (JSON.stringify(prevTasks) === JSON.stringify(newTasks)) { - return prevTasks; // Return same reference to prevent re-render - } - - setSelectedTask((prev) => { - if (!prev) return prev; - - const updatedSelectedTask = newTasks.find( - (task) => task.id === prev.id - ); - - if ( - JSON.stringify(prev) === JSON.stringify(updatedSelectedTask) - ) - return prev; - return updatedSelectedTask || prev; - }); - - return newTasks; - }); + const result = await tasksApi.getAll(projectId!); + // Only update if data has actually changed + setTasks((prevTasks) => { + const newTasks = result; + if (JSON.stringify(prevTasks) === JSON.stringify(newTasks)) { + return prevTasks; // Return same reference to prevent re-render } - } else { - setError('Failed to load tasks'); - } + + setSelectedTask((prev) => { + if (!prev) return prev; + + const updatedSelectedTask = newTasks.find( + (task) => task.id === prev.id + ); + + if (JSON.stringify(prev) === JSON.stringify(updatedSelectedTask)) + return prev; + return updatedSelectedTask || prev; + }); + + return newTasks; + }); } catch (err) { setError('Failed to load tasks'); } finally { @@ -186,20 +152,12 @@ export function ProjectTasks() { const handleCreateTask = useCallback( async (title: string, description: string) => { try { - const response = await makeRequest(`/api/projects/${projectId}/tasks`, { - method: 'POST', - body: JSON.stringify({ - project_id: projectId, - title, - description: description || null, - }), + await tasksApi.create(projectId!, { + project_id: projectId!, + title, + description: description || null, }); - - if (response.ok) { - await fetchTasks(); - } else { - setError('Failed to create task'); - } + await fetchTasks(); } catch (err) { setError('Failed to create task'); } @@ -216,25 +174,10 @@ export function ProjectTasks() { description: description || null, executor: executor || null, }; - - const response = await makeRequest( - `/api/projects/${projectId}/tasks/create-and-start`, - { - method: 'POST', - body: JSON.stringify(payload), - } - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - await fetchTasks(); - // Open the newly created task in the details panel - handleViewTaskDetails(result.data); - } - } else { - setError('Failed to create and start task'); - } + const result = await tasksApi.createAndStart(projectId!, payload); + await fetchTasks(); + // Open the newly created task in the details panel + handleViewTaskDetails(result); } catch (err) { setError('Failed to create and start task'); } @@ -247,24 +190,13 @@ export function ProjectTasks() { if (!editingTask) return; try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${editingTask.id}`, - { - method: 'PUT', - body: JSON.stringify({ - title, - description: description || null, - status, - }), - } - ); - - if (response.ok) { - await fetchTasks(); - setEditingTask(null); - } else { - setError('Failed to update task'); - } + await tasksApi.update(projectId!, editingTask.id, { + title, + description: description || null, + status, + }); + await fetchTasks(); + setEditingTask(null); } catch (err) { setError('Failed to update task'); } @@ -277,19 +209,9 @@ export function ProjectTasks() { if (!confirm('Are you sure you want to delete this task?')) return; try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${taskId}`, - { - method: 'DELETE', - } - ); - - if (response.ok) { - await fetchTasks(); - } else { - setError('Failed to delete task'); - } - } catch (err) { + await tasksApi.delete(projectId!, taskId); + await fetchTasks(); + } catch (error) { setError('Failed to delete task'); } }, @@ -303,8 +225,8 @@ export function ProjectTasks() { const handleViewTaskDetails = useCallback( (task: Task) => { - setSelectedTask(task); - setIsPanelOpen(true); + // setSelectedTask(task); + // setIsPanelOpen(true); // Update URL to include task ID navigate(`/projects/${projectId}/tasks/${task.id}`, { replace: true }); }, @@ -312,8 +234,8 @@ export function ProjectTasks() { ); const handleClosePanel = useCallback(() => { - setIsPanelOpen(false); - setSelectedTask(null); + // setIsPanelOpen(false); + // setSelectedTask(null); // Remove task ID from URL when closing panel navigate(`/projects/${projectId}/tasks`, { replace: true }); }, [projectId, navigate]); @@ -342,27 +264,11 @@ export function ProjectTasks() { ); try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${taskId}`, - { - method: 'PUT', - body: JSON.stringify({ - title: task.title, - description: task.description, - status: newStatus, - }), - } - ); - - if (!response.ok) { - // Revert the optimistic update if the API call failed - setTasks((prev) => - prev.map((t) => - t.id === taskId ? { ...t, status: previousStatus } : t - ) - ); - setError('Failed to update task status'); - } + await tasksApi.update(projectId!, taskId, { + title: task.title, + description: task.description, + status: newStatus, + }); } catch (err) { // Revert the optimistic update if the API call failed setTasks((prev) => @@ -377,13 +283,12 @@ export function ProjectTasks() { ); if (loading) { - return
Loading tasks...
; + return ; } if (error) { return
{error}
; } - console.log('selectedTask', selectedTask); return (
@@ -470,7 +375,6 @@ export function ProjectTasks() { task={selectedTask} projectHasDevScript={!!project?.dev_script} projectId={projectId!} - isOpen={isPanelOpen} onClose={handleClosePanel} onEditTask={handleEditTask} onDeleteTask={handleDeleteTask} diff --git a/shared/types.ts b/shared/types.ts index c188c54b..856f8040 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -4,35 +4,90 @@ export type ApiResponse = { success: boolean, data: T | null, message: string | null, }; -export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, telemetry_acknowledged: boolean, sound_alerts: boolean, sound_file: SoundFile, push_notifications: boolean, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean | null, }; +export type Config = { + theme: ThemeMode, + executor: ExecutorConfig, + disclaimer_acknowledged: boolean, + onboarding_acknowledged: boolean, + telemetry_acknowledged: boolean, + sound_alerts: boolean, + sound_file: SoundFile, + push_notifications: boolean, + editor: EditorConfig, + github: GitHubConfig, + analytics_enabled: boolean | null, +}; export type ThemeMode = "light" | "dark" | "system" | "purple" | "green" | "blue" | "orange" | "red"; export type EditorConfig = { editor_type: EditorType, custom_command: string | null, }; -export type GitHubConfig = { pat: string | null, token: string | null, username: string | null, primary_email: string | null, default_pr_base: string | null, }; +export type GitHubConfig = { + pat: string | null, + token: string | null, + username: string | null, + primary_email: string | null, + default_pr_base: string | null, +}; export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom"; export type EditorConstants = { editor_types: Array, editor_labels: Array, }; -export type SoundFile = "abstract-sound1" | "abstract-sound2" | "abstract-sound3" | "abstract-sound4" | "cow-mooing" | "phone-vibration" | "rooster"; +export type SoundFile = + "abstract-sound1" + | "abstract-sound2" + | "abstract-sound3" + | "abstract-sound4" + | "cow-mooing" + | "phone-vibration" + | "rooster"; export type SoundConstants = { sound_files: Array, sound_labels: Array, }; export type ConfigConstants = { editor: EditorConstants, sound: SoundConstants, }; -export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | { "type": "opencode" }; +export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | { + "type": "opencode" +}; export type ExecutorConstants = { executor_types: Array, executor_labels: Array, }; -export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, dev_script: string | null, }; +export type CreateProject = { + name: string, + git_repo_path: string, + use_existing_repo: boolean, + setup_script: string | null, + dev_script: string | null, +}; -export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, created_at: Date, updated_at: Date, }; +export type Project = { + id: string, + name: string, + git_repo_path: string, + setup_script: string | null, + dev_script: string | null, + created_at: Date, + updated_at: Date, +}; -export type ProjectWithBranch = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, current_branch: string | null, created_at: Date, updated_at: Date, }; +export type ProjectWithBranch = { + id: string, + name: string, + git_repo_path: string, + setup_script: string | null, + dev_script: string | null, + current_branch: string | null, + created_at: Date, + updated_at: Date, +}; -export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, }; +export type UpdateProject = { + name: string | null, + git_repo_path: string | null, + setup_script: string | null, + dev_script: string | null, +}; export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, }; @@ -44,19 +99,65 @@ export type CreateBranch = { name: string, base_branch: string | null, }; export type CreateTask = { project_id: string, title: string, description: string | null, }; -export type CreateTaskAndStart = { project_id: string, title: string, description: string | null, executor: ExecutorConfig | null, }; +export type CreateTaskAndStart = { + project_id: string, + title: string, + description: string | null, + executor: ExecutorConfig | null, +}; export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled"; -export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, }; +export type Task = { + id: string, + project_id: string, + title: string, + description: string | null, + status: TaskStatus, + created_at: string, + updated_at: string, +}; -export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, has_failed_attempt: boolean, }; +export type TaskWithAttemptStatus = { + id: string, + project_id: string, + title: string, + description: string | null, + status: TaskStatus, + created_at: string, + updated_at: string, + has_in_progress_attempt: boolean, + has_merged_attempt: boolean, + has_failed_attempt: boolean, +}; export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, }; -export type TaskAttemptStatus = "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed"; +export type TaskAttemptStatus = + "setuprunning" + | "setupcomplete" + | "setupfailed" + | "executorrunning" + | "executorcomplete" + | "executorfailed"; -export type TaskAttempt = { id: string, task_id: string, worktree_path: string, branch: string, base_branch: string, merge_commit: string | null, executor: string | null, pr_url: string | null, pr_number: bigint | null, pr_status: string | null, pr_merged_at: string | null, worktree_deleted: boolean, setup_completed_at: string | null, created_at: string, updated_at: string, }; +export type TaskAttempt = { + id: string, + task_id: string, + worktree_path: string, + branch: string, + base_branch: string, + merge_commit: string | null, + executor: string | null, + pr_url: string | null, + pr_number: bigint | null, + pr_status: string | null, + pr_merged_at: string | null, + worktree_deleted: boolean, + setup_completed_at: string | null, + created_at: string, + updated_at: string, +}; export type CreateTaskAttempt = { executor: string | null, base_branch: string | null, }; @@ -64,11 +165,28 @@ export type UpdateTaskAttempt = Record; export type CreateFollowUpAttempt = { prompt: string, }; -export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, }; +export type TaskAttemptActivity = { + id: string, + execution_process_id: string, + status: TaskAttemptStatus, + note: string | null, + created_at: string, +}; -export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, }; +export type TaskAttemptActivityWithPrompt = { + id: string, + execution_process_id: string, + status: TaskAttemptStatus, + note: string | null, + created_at: string, + prompt: string | null, +}; -export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, }; +export type CreateTaskAttemptActivity = { + execution_process_id: string, + status: TaskAttemptStatus | null, + note: string | null, +}; export type AttemptData = { activities: TaskAttemptActivityWithPrompt[]; @@ -101,37 +219,133 @@ export type FileDiff = { path: string, chunks: Array, }; export type WorktreeDiff = { files: Array, }; -export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, has_uncommitted_changes: boolean, base_branch_name: string, }; +export type BranchStatus = { + is_behind: boolean, + commits_behind: number, + commits_ahead: number, + up_to_date: boolean, + merged: boolean, + has_uncommitted_changes: boolean, + base_branch_name: string, +}; -export type ExecutionState = "NotStarted" | "SetupRunning" | "SetupComplete" | "SetupFailed" | "CodingAgentRunning" | "CodingAgentComplete" | "CodingAgentFailed" | "Complete"; +export type ExecutionState = + "NotStarted" + | "SetupRunning" + | "SetupComplete" + | "SetupFailed" + | "CodingAgentRunning" + | "CodingAgentComplete" + | "CodingAgentFailed" + | "Complete"; -export type TaskAttemptState = { execution_state: ExecutionState, has_changes: boolean, has_setup_script: boolean, setup_process_id: string | null, coding_agent_process_id: string | null, }; +export type TaskAttemptState = { + execution_state: ExecutionState, + has_changes: boolean, + has_setup_script: boolean, + setup_process_id: string | null, + coding_agent_process_id: string | null, +}; -export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, stdout: string | null, stderr: string | null, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, }; +export type ExecutionProcess = { + id: string, + task_attempt_id: string, + process_type: ExecutionProcessType, + executor_type: string | null, + status: ExecutionProcessStatus, + command: string, + args: string | null, + working_directory: string, + stdout: string | null, + stderr: string | null, + exit_code: bigint | null, + started_at: string, + completed_at: string | null, + created_at: string, + updated_at: string, +}; -export type ExecutionProcessSummary = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, }; +export type ExecutionProcessSummary = { + id: string, + task_attempt_id: string, + process_type: ExecutionProcessType, + executor_type: string | null, + status: ExecutionProcessStatus, + command: string, + args: string | null, + working_directory: string, + exit_code: bigint | null, + started_at: string, + completed_at: string | null, + created_at: string, + updated_at: string, +}; export type ExecutionProcessStatus = "running" | "completed" | "failed" | "killed"; export type ExecutionProcessType = "setupscript" | "codingagent" | "devserver"; -export type CreateExecutionProcess = { task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, command: string, args: string | null, working_directory: string, }; +export type CreateExecutionProcess = { + task_attempt_id: string, + process_type: ExecutionProcessType, + executor_type: string | null, + command: string, + args: string | null, + working_directory: string, +}; -export type UpdateExecutionProcess = { status: ExecutionProcessStatus | null, exit_code: bigint | null, completed_at: string | null, }; +export type UpdateExecutionProcess = { + status: ExecutionProcessStatus | null, + exit_code: bigint | null, + completed_at: string | null, +}; -export type ExecutorSession = { id: string, task_attempt_id: string, execution_process_id: string, session_id: string | null, prompt: string | null, summary: string | null, created_at: string, updated_at: string, }; +export type ExecutorSession = { + id: string, + task_attempt_id: string, + execution_process_id: string, + session_id: string | null, + prompt: string | null, + summary: string | null, + created_at: string, + updated_at: string, +}; export type CreateExecutorSession = { task_attempt_id: string, execution_process_id: string, prompt: string | null, }; export type UpdateExecutorSession = { session_id: string | null, prompt: string | null, summary: string | null, }; -export type NormalizedConversation = { entries: Array, session_id: string | null, executor_type: string, prompt: string | null, summary: string | null, }; +export type NormalizedConversation = { + entries: Array, + session_id: string | null, + executor_type: string, + prompt: string | null, + summary: string | null, +}; export type NormalizedEntry = { timestamp: string | null, entry_type: NormalizedEntryType, content: string, }; -export type NormalizedEntryType = { "type": "user_message" } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, } | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" }; +export type NormalizedEntryType = { "type": "user_message" } | { "type": "assistant_message" } | { + "type": "tool_use", + tool_name: string, + action_type: ActionType, +} | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" }; -export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | { "action": "command_run", command: string, } | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { "action": "task_create", description: string, } | { "action": "other", description: string, }; +export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | { + "action": "command_run", + command: string, +} | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { + "action": "task_create", + description: string, +} | { "action": "other", description: string, }; + +export type StartGitHubDeviceFlowType = { + device_code: string; + user_code: string; + verification_uri: string; + expires_in: number; + interval: number; +}; // Generated constants export const EXECUTOR_TYPES: string[] = [ @@ -144,7 +358,7 @@ export const EXECUTOR_TYPES: string[] = [ export const EDITOR_TYPES: EditorType[] = [ "vscode", - "cursor", + "cursor", "windsurf", "intellij", "zed",