mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 22:32:06 +00:00
Introduce a new testing agent architecture that runs regression tests independently from coding agents, improving quality assurance in parallel mode. Key changes: Testing Agent System: - Add testing_prompt.template.md for dedicated testing agent role - Add feature_mark_failing MCP tool for regression detection - Add --agent-type flag to select initializer/coding/testing mode - Remove regression testing from coding prompt (now handled by testing agents) Parallel Orchestrator Enhancements: - Add testing agent spawning with configurable ratio (--testing-agent-ratio) - Add comprehensive debug logging system (DebugLog class) - Improve database session management to prevent stale reads - Add engine.dispose() calls to refresh connections after subprocess commits - Fix f-string linting issues (remove unnecessary f-prefixes) UI Improvements: - Add testing agent mascot (Chip) to AgentAvatar - Enhance AgentCard to display testing agent status - Add testing agent ratio slider in SettingsModal - Update WebSocket handling for testing agent updates - Improve ActivityFeed to show testing agent activity API & Server Updates: - Add testing_agent_ratio to settings schema and endpoints - Update process manager to support testing agent type - Enhance WebSocket messages for agent_update events Template Changes: - Delete coding_prompt_yolo.template.md (consolidated into main prompt) - Update initializer_prompt.template.md with improved structure - Streamline coding_prompt.template.md workflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
444 lines
13 KiB
TypeScript
444 lines
13 KiB
TypeScript
/**
|
|
* API Client for the Autonomous Coding UI
|
|
*/
|
|
|
|
import type {
|
|
ProjectSummary,
|
|
ProjectDetail,
|
|
ProjectPrompts,
|
|
FeatureListResponse,
|
|
Feature,
|
|
FeatureCreate,
|
|
FeatureUpdate,
|
|
FeatureBulkCreate,
|
|
FeatureBulkCreateResponse,
|
|
DependencyGraph,
|
|
AgentStatusResponse,
|
|
AgentActionResponse,
|
|
SetupStatus,
|
|
DirectoryListResponse,
|
|
PathValidationResponse,
|
|
AssistantConversation,
|
|
AssistantConversationDetail,
|
|
Settings,
|
|
SettingsUpdate,
|
|
ModelsResponse,
|
|
DevServerStatusResponse,
|
|
DevServerConfig,
|
|
TerminalInfo,
|
|
} from './types'
|
|
|
|
const API_BASE = '/api'
|
|
|
|
async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
|
|
const response = await fetch(`${API_BASE}${url}`, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options?.headers,
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
|
|
throw new Error(error.detail || `HTTP ${response.status}`)
|
|
}
|
|
|
|
return response.json()
|
|
}
|
|
|
|
// ============================================================================
|
|
// Projects API
|
|
// ============================================================================
|
|
|
|
export async function listProjects(): Promise<ProjectSummary[]> {
|
|
return fetchJSON('/projects')
|
|
}
|
|
|
|
export async function createProject(
|
|
name: string,
|
|
path: string,
|
|
specMethod: 'claude' | 'manual' = 'manual'
|
|
): Promise<ProjectSummary> {
|
|
return fetchJSON('/projects', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, path, spec_method: specMethod }),
|
|
})
|
|
}
|
|
|
|
export async function getProject(name: string): Promise<ProjectDetail> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(name)}`)
|
|
}
|
|
|
|
export async function deleteProject(name: string): Promise<void> {
|
|
await fetchJSON(`/projects/${encodeURIComponent(name)}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
export async function getProjectPrompts(name: string): Promise<ProjectPrompts> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`)
|
|
}
|
|
|
|
export async function updateProjectPrompts(
|
|
name: string,
|
|
prompts: Partial<ProjectPrompts>
|
|
): Promise<void> {
|
|
await fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(prompts),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Features API
|
|
// ============================================================================
|
|
|
|
export async function listFeatures(projectName: string): Promise<FeatureListResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`)
|
|
}
|
|
|
|
export async function createFeature(projectName: string, feature: FeatureCreate): Promise<Feature> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(feature),
|
|
})
|
|
}
|
|
|
|
export async function getFeature(projectName: string, featureId: number): Promise<Feature> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`)
|
|
}
|
|
|
|
export async function deleteFeature(projectName: string, featureId: number): Promise<void> {
|
|
await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
export async function skipFeature(projectName: string, featureId: number): Promise<void> {
|
|
await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}/skip`, {
|
|
method: 'PATCH',
|
|
})
|
|
}
|
|
|
|
export async function updateFeature(
|
|
projectName: string,
|
|
featureId: number,
|
|
update: FeatureUpdate
|
|
): Promise<Feature> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(update),
|
|
})
|
|
}
|
|
|
|
export async function createFeaturesBulk(
|
|
projectName: string,
|
|
bulk: FeatureBulkCreate
|
|
): Promise<FeatureBulkCreateResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/bulk`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(bulk),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Dependency Graph API
|
|
// ============================================================================
|
|
|
|
export async function getDependencyGraph(projectName: string): Promise<DependencyGraph> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/graph`)
|
|
}
|
|
|
|
export async function addDependency(
|
|
projectName: string,
|
|
featureId: number,
|
|
dependencyId: number
|
|
): Promise<{ success: boolean; feature_id: number; dependencies: number[] }> {
|
|
return fetchJSON(
|
|
`/projects/${encodeURIComponent(projectName)}/features/${featureId}/dependencies/${dependencyId}`,
|
|
{ method: 'POST' }
|
|
)
|
|
}
|
|
|
|
export async function removeDependency(
|
|
projectName: string,
|
|
featureId: number,
|
|
dependencyId: number
|
|
): Promise<{ success: boolean; feature_id: number; dependencies: number[] }> {
|
|
return fetchJSON(
|
|
`/projects/${encodeURIComponent(projectName)}/features/${featureId}/dependencies/${dependencyId}`,
|
|
{ method: 'DELETE' }
|
|
)
|
|
}
|
|
|
|
export async function setDependencies(
|
|
projectName: string,
|
|
featureId: number,
|
|
dependencyIds: number[]
|
|
): Promise<{ success: boolean; feature_id: number; dependencies: number[] }> {
|
|
return fetchJSON(
|
|
`/projects/${encodeURIComponent(projectName)}/features/${featureId}/dependencies`,
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify({ dependency_ids: dependencyIds }),
|
|
}
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Agent API
|
|
// ============================================================================
|
|
|
|
export async function getAgentStatus(projectName: string): Promise<AgentStatusResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/status`)
|
|
}
|
|
|
|
export async function startAgent(
|
|
projectName: string,
|
|
options: {
|
|
yoloMode?: boolean
|
|
parallelMode?: boolean
|
|
maxConcurrency?: number
|
|
testingAgentRatio?: number
|
|
countTestingInConcurrency?: boolean
|
|
} = {}
|
|
): Promise<AgentActionResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/start`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
yolo_mode: options.yoloMode ?? false,
|
|
parallel_mode: options.parallelMode ?? false,
|
|
max_concurrency: options.maxConcurrency,
|
|
testing_agent_ratio: options.testingAgentRatio,
|
|
count_testing_in_concurrency: options.countTestingInConcurrency,
|
|
}),
|
|
})
|
|
}
|
|
|
|
export async function stopAgent(projectName: string): Promise<AgentActionResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/stop`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
export async function pauseAgent(projectName: string): Promise<AgentActionResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/pause`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
export async function resumeAgent(projectName: string): Promise<AgentActionResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/resume`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Spec Creation API
|
|
// ============================================================================
|
|
|
|
export interface SpecFileStatus {
|
|
exists: boolean
|
|
status: 'complete' | 'in_progress' | 'not_started' | 'error' | 'unknown'
|
|
feature_count: number | null
|
|
timestamp: string | null
|
|
files_written: string[]
|
|
}
|
|
|
|
export async function getSpecStatus(projectName: string): Promise<SpecFileStatus> {
|
|
return fetchJSON(`/spec/status/${encodeURIComponent(projectName)}`)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Setup API
|
|
// ============================================================================
|
|
|
|
export async function getSetupStatus(): Promise<SetupStatus> {
|
|
return fetchJSON('/setup/status')
|
|
}
|
|
|
|
export async function healthCheck(): Promise<{ status: string }> {
|
|
return fetchJSON('/health')
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filesystem API
|
|
// ============================================================================
|
|
|
|
export async function listDirectory(path?: string): Promise<DirectoryListResponse> {
|
|
const params = path ? `?path=${encodeURIComponent(path)}` : ''
|
|
return fetchJSON(`/filesystem/list${params}`)
|
|
}
|
|
|
|
export async function createDirectory(fullPath: string): Promise<{ success: boolean; path: string }> {
|
|
// Backend expects { parent_path, name }, not { path }
|
|
// Split the full path into parent directory and folder name
|
|
|
|
// Remove trailing slash if present
|
|
const normalizedPath = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath
|
|
|
|
// Find the last path separator
|
|
const lastSlash = normalizedPath.lastIndexOf('/')
|
|
|
|
let parentPath: string
|
|
let name: string
|
|
|
|
// Handle Windows drive root (e.g., "C:/newfolder")
|
|
if (lastSlash === 2 && /^[A-Za-z]:/.test(normalizedPath)) {
|
|
// Path like "C:/newfolder" - parent is "C:/"
|
|
parentPath = normalizedPath.substring(0, 3) // "C:/"
|
|
name = normalizedPath.substring(3)
|
|
} else if (lastSlash > 0) {
|
|
parentPath = normalizedPath.substring(0, lastSlash)
|
|
name = normalizedPath.substring(lastSlash + 1)
|
|
} else if (lastSlash === 0) {
|
|
// Unix root path like "/newfolder"
|
|
parentPath = '/'
|
|
name = normalizedPath.substring(1)
|
|
} else {
|
|
// No slash - invalid path
|
|
throw new Error('Invalid path: must be an absolute path')
|
|
}
|
|
|
|
if (!name) {
|
|
throw new Error('Invalid path: directory name is empty')
|
|
}
|
|
|
|
return fetchJSON('/filesystem/create-directory', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ parent_path: parentPath, name }),
|
|
})
|
|
}
|
|
|
|
export async function validatePath(path: string): Promise<PathValidationResponse> {
|
|
return fetchJSON('/filesystem/validate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ path }),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Assistant Chat API
|
|
// ============================================================================
|
|
|
|
export async function listAssistantConversations(
|
|
projectName: string
|
|
): Promise<AssistantConversation[]> {
|
|
return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`)
|
|
}
|
|
|
|
export async function getAssistantConversation(
|
|
projectName: string,
|
|
conversationId: number
|
|
): Promise<AssistantConversationDetail> {
|
|
return fetchJSON(
|
|
`/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`
|
|
)
|
|
}
|
|
|
|
export async function createAssistantConversation(
|
|
projectName: string
|
|
): Promise<AssistantConversation> {
|
|
return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
export async function deleteAssistantConversation(
|
|
projectName: string,
|
|
conversationId: number
|
|
): Promise<void> {
|
|
await fetchJSON(
|
|
`/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`,
|
|
{ method: 'DELETE' }
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Settings API
|
|
// ============================================================================
|
|
|
|
export async function getAvailableModels(): Promise<ModelsResponse> {
|
|
return fetchJSON('/settings/models')
|
|
}
|
|
|
|
export async function getSettings(): Promise<Settings> {
|
|
return fetchJSON('/settings')
|
|
}
|
|
|
|
export async function updateSettings(settings: SettingsUpdate): Promise<Settings> {
|
|
return fetchJSON('/settings', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(settings),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Dev Server API
|
|
// ============================================================================
|
|
|
|
export async function getDevServerStatus(projectName: string): Promise<DevServerStatusResponse> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/status`)
|
|
}
|
|
|
|
export async function startDevServer(
|
|
projectName: string,
|
|
command?: string
|
|
): Promise<{ success: boolean; message: string }> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/start`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ command }),
|
|
})
|
|
}
|
|
|
|
export async function stopDevServer(
|
|
projectName: string
|
|
): Promise<{ success: boolean; message: string }> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/stop`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
export async function getDevServerConfig(projectName: string): Promise<DevServerConfig> {
|
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/config`)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Terminal API
|
|
// ============================================================================
|
|
|
|
export async function listTerminals(projectName: string): Promise<TerminalInfo[]> {
|
|
return fetchJSON(`/terminal/${encodeURIComponent(projectName)}`)
|
|
}
|
|
|
|
export async function createTerminal(
|
|
projectName: string,
|
|
name?: string
|
|
): Promise<TerminalInfo> {
|
|
return fetchJSON(`/terminal/${encodeURIComponent(projectName)}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name: name ?? null }),
|
|
})
|
|
}
|
|
|
|
export async function renameTerminal(
|
|
projectName: string,
|
|
terminalId: string,
|
|
name: string
|
|
): Promise<TerminalInfo> {
|
|
return fetchJSON(`/terminal/${encodeURIComponent(projectName)}/${terminalId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ name }),
|
|
})
|
|
}
|
|
|
|
export async function deleteTerminal(
|
|
projectName: string,
|
|
terminalId: string
|
|
): Promise<void> {
|
|
await fetchJSON(`/terminal/${encodeURIComponent(projectName)}/${terminalId}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|