Files
autocoder/ui/src/lib/api.ts
Caitlyn Byrne 656df0fd9a feat: add "blocked for human input" feature across full stack
Agents can now request structured human input when they encounter
genuine blockers (API keys, design choices, external configs). The
request is displayed in the UI with a dynamic form, and the human's
response is stored and made available when the agent resumes.

Changes span 21 files + 1 new component:
- Database: 3 new columns (needs_human_input, human_input_request,
  human_input_response) with migration
- MCP: new feature_request_human_input tool + guards on existing tools
- API: new resolve-human-input endpoint, 4th feature bucket
- Orchestrator: skip needs_human_input features in scheduling
- Progress: 4-tuple return from count_passing_tests
- WebSocket: needs_human_input count in progress messages
- UI: conditional 4th Kanban column, HumanInputForm component,
  amber status indicators, dependency graph support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:11:35 -05:00

555 lines
16 KiB
TypeScript

/**
* API Client for the Autonomous Coding UI
*/
import type {
ProjectSummary,
ProjectDetail,
ProjectPrompts,
ProjectSettingsUpdate,
FeatureListResponse,
Feature,
FeatureCreate,
FeatureUpdate,
FeatureBulkCreate,
FeatureBulkCreateResponse,
DependencyGraph,
AgentStatusResponse,
AgentActionResponse,
SetupStatus,
DirectoryListResponse,
PathValidationResponse,
AssistantConversation,
AssistantConversationDetail,
Settings,
SettingsUpdate,
ModelsResponse,
ProvidersResponse,
DevServerStatusResponse,
DevServerConfig,
TerminalInfo,
Schedule,
ScheduleCreate,
ScheduleUpdate,
ScheduleListResponse,
NextRunResponse,
} 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}`)
}
// Handle 204 No Content responses
if (response.status === 204) {
return undefined as T
}
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),
})
}
export async function updateProjectSettings(
name: string,
settings: ProjectSettingsUpdate
): Promise<ProjectDetail> {
return fetchJSON(`/projects/${encodeURIComponent(name)}/settings`, {
method: 'PATCH',
body: JSON.stringify(settings),
})
}
export interface ResetProjectResponse {
success: boolean
reset_type: 'quick' | 'full'
deleted_files: string[]
message: string
}
export async function resetProject(
name: string,
fullReset: boolean = false
): Promise<ResetProjectResponse> {
const params = fullReset ? '?full_reset=true' : ''
return fetchJSON(`/projects/${encodeURIComponent(name)}/reset${params}`, {
method: 'POST',
})
}
// ============================================================================
// 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),
})
}
export async function resolveHumanInput(
projectName: string,
featureId: number,
response: { fields: Record<string, string | boolean | string[]> }
): Promise<Feature> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}/resolve-human-input`, {
method: 'POST',
body: JSON.stringify(response),
})
}
// ============================================================================
// 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
} = {}
): 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,
}),
})
}
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 getAvailableProviders(): Promise<ProvidersResponse> {
return fetchJSON('/settings/providers')
}
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`)
}
export async function updateDevServerConfig(
projectName: string,
customCommand: string | null
): Promise<DevServerConfig> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/config`, {
method: 'PATCH',
body: JSON.stringify({ custom_command: customCommand }),
})
}
// ============================================================================
// 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',
})
}
// ============================================================================
// Schedule API
// ============================================================================
export async function listSchedules(projectName: string): Promise<ScheduleListResponse> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/schedules`)
}
export async function createSchedule(
projectName: string,
schedule: ScheduleCreate
): Promise<Schedule> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/schedules`, {
method: 'POST',
body: JSON.stringify(schedule),
})
}
export async function getSchedule(
projectName: string,
scheduleId: number
): Promise<Schedule> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/schedules/${scheduleId}`)
}
export async function updateSchedule(
projectName: string,
scheduleId: number,
update: ScheduleUpdate
): Promise<Schedule> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/schedules/${scheduleId}`, {
method: 'PATCH',
body: JSON.stringify(update),
})
}
export async function deleteSchedule(
projectName: string,
scheduleId: number
): Promise<void> {
await fetchJSON(`/projects/${encodeURIComponent(projectName)}/schedules/${scheduleId}`, {
method: 'DELETE',
})
}
export async function getNextScheduledRun(projectName: string): Promise<NextRunResponse> {
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/schedules/next`)
}