diff --git a/mcp_server/feature_mcp.py b/mcp_server/feature_mcp.py index 28c306e..4e0b08b 100755 --- a/mcp_server/feature_mcp.py +++ b/mcp_server/feature_mcp.py @@ -1032,9 +1032,25 @@ def feature_request_human_input( JSON with success confirmation or error message """ # Validate fields + VALID_FIELD_TYPES = {"text", "textarea", "select", "boolean"} + seen_ids: set[str] = set() for i, field in enumerate(fields): if "id" not in field or "label" not in field: return json.dumps({"error": f"Field at index {i} missing required 'id' or 'label'"}) + fid = field["id"] + flabel = field["label"] + if not isinstance(fid, str) or not fid.strip(): + return json.dumps({"error": f"Field at index {i} has empty or invalid 'id'"}) + if not isinstance(flabel, str) or not flabel.strip(): + return json.dumps({"error": f"Field at index {i} has empty or invalid 'label'"}) + if fid in seen_ids: + return json.dumps({"error": f"Duplicate field id '{fid}' at index {i}"}) + seen_ids.add(fid) + ftype = field.get("type", "text") + if ftype not in VALID_FIELD_TYPES: + return json.dumps({"error": f"Field at index {i} has invalid type '{ftype}'. Must be one of: {', '.join(sorted(VALID_FIELD_TYPES))}"}) + if ftype == "select" and not field.get("options"): + return json.dumps({"error": f"Field at index {i} is type 'select' but missing 'options' array"}) request_data = { "prompt": prompt, @@ -1050,7 +1066,7 @@ def feature_request_human_input( in_progress = 0, human_input_request = :request, human_input_response = NULL - WHERE id = :id AND passes = 0 + WHERE id = :id AND passes = 0 AND in_progress = 1 """), {"id": feature_id, "request": json.dumps(request_data)}) session.commit() @@ -1060,6 +1076,8 @@ def feature_request_human_input( return json.dumps({"error": f"Feature with ID {feature_id} not found"}) if feature.passes: return json.dumps({"error": f"Feature with ID {feature_id} is already passing"}) + if not feature.in_progress: + return json.dumps({"error": f"Feature with ID {feature_id} is not in progress"}) return json.dumps({"error": "Failed to request human input for unknown reason"}) feature = session.query(Feature).filter(Feature.id == feature_id).first() diff --git a/ui/src/App.tsx b/ui/src/App.tsx index d80784e..02e1db5 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -130,7 +130,8 @@ function App() { const allFeatures = [ ...(features?.pending ?? []), ...(features?.in_progress ?? []), - ...(features?.done ?? []) + ...(features?.done ?? []), + ...(features?.needs_human_input ?? []) ] const feature = allFeatures.find(f => f.id === nodeId) if (feature) setSelectedFeature(feature) @@ -245,7 +246,7 @@ function App() { // Combine WebSocket progress with feature data const progress = wsState.progress.total > 0 ? wsState.progress : { passing: features?.done.length ?? 0, - total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0), + total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0) + (features?.needs_human_input?.length ?? 0), percentage: 0, } diff --git a/ui/src/hooks/useWebSocket.ts b/ui/src/hooks/useWebSocket.ts index b9c0a3f..edcc604 100644 --- a/ui/src/hooks/useWebSocket.ts +++ b/ui/src/hooks/useWebSocket.ts @@ -33,6 +33,7 @@ interface WebSocketState { progress: { passing: number in_progress: number + needs_human_input: number total: number percentage: number } @@ -60,7 +61,7 @@ const MAX_AGENT_LOGS = 500 // Keep last 500 log lines per agent export function useProjectWebSocket(projectName: string | null) { const [state, setState] = useState({ - progress: { passing: 0, in_progress: 0, total: 0, percentage: 0 }, + progress: { passing: 0, in_progress: 0, needs_human_input: 0, total: 0, percentage: 0 }, agentStatus: 'loading', logs: [], isConnected: false, @@ -107,6 +108,7 @@ export function useProjectWebSocket(projectName: string | null) { progress: { passing: message.passing, in_progress: message.in_progress, + needs_human_input: message.needs_human_input ?? 0, total: message.total, percentage: message.percentage, }, @@ -385,7 +387,7 @@ export function useProjectWebSocket(projectName: string | null) { // Reset state when project changes to clear stale data // Use 'loading' for agentStatus to show loading indicator until WebSocket provides actual status setState({ - progress: { passing: 0, in_progress: 0, total: 0, percentage: 0 }, + progress: { passing: 0, in_progress: 0, needs_human_input: 0, total: 0, percentage: 0 }, agentStatus: 'loading', logs: [], isConnected: false,