mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-16 18:33:08 +00:00
fix: address PR #184 review findings for blocked-for-human-input feature
A) Graph view: add needs_human_input bucket to handleGraphNodeClick so clicking blocked nodes opens the feature modal B) MCP validation: validate field type enum, require options for select, enforce unique non-empty field IDs and labels C) Progress fallback: include needs_human_input in non-WebSocket total D) WebSocket: track needs_human_input count in progress state E) Cleanup guard: remove unnecessary needs_human_input check in _cleanup_stale_features (resolved via merge conflict) F) Defensive SQL: require in_progress=1 in feature_request_human_input Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1032,9 +1032,25 @@ def feature_request_human_input(
|
|||||||
JSON with success confirmation or error message
|
JSON with success confirmation or error message
|
||||||
"""
|
"""
|
||||||
# Validate fields
|
# Validate fields
|
||||||
|
VALID_FIELD_TYPES = {"text", "textarea", "select", "boolean"}
|
||||||
|
seen_ids: set[str] = set()
|
||||||
for i, field in enumerate(fields):
|
for i, field in enumerate(fields):
|
||||||
if "id" not in field or "label" not in field:
|
if "id" not in field or "label" not in field:
|
||||||
return json.dumps({"error": f"Field at index {i} missing required 'id' or 'label'"})
|
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 = {
|
request_data = {
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
@@ -1050,7 +1066,7 @@ def feature_request_human_input(
|
|||||||
in_progress = 0,
|
in_progress = 0,
|
||||||
human_input_request = :request,
|
human_input_request = :request,
|
||||||
human_input_response = NULL
|
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)})
|
"""), {"id": feature_id, "request": json.dumps(request_data)})
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -1060,6 +1076,8 @@ def feature_request_human_input(
|
|||||||
return json.dumps({"error": f"Feature with ID {feature_id} not found"})
|
return json.dumps({"error": f"Feature with ID {feature_id} not found"})
|
||||||
if feature.passes:
|
if feature.passes:
|
||||||
return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})
|
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"})
|
return json.dumps({"error": "Failed to request human input for unknown reason"})
|
||||||
|
|
||||||
feature = session.query(Feature).filter(Feature.id == feature_id).first()
|
feature = session.query(Feature).filter(Feature.id == feature_id).first()
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ function App() {
|
|||||||
const allFeatures = [
|
const allFeatures = [
|
||||||
...(features?.pending ?? []),
|
...(features?.pending ?? []),
|
||||||
...(features?.in_progress ?? []),
|
...(features?.in_progress ?? []),
|
||||||
...(features?.done ?? [])
|
...(features?.done ?? []),
|
||||||
|
...(features?.needs_human_input ?? [])
|
||||||
]
|
]
|
||||||
const feature = allFeatures.find(f => f.id === nodeId)
|
const feature = allFeatures.find(f => f.id === nodeId)
|
||||||
if (feature) setSelectedFeature(feature)
|
if (feature) setSelectedFeature(feature)
|
||||||
@@ -245,7 +246,7 @@ function App() {
|
|||||||
// Combine WebSocket progress with feature data
|
// Combine WebSocket progress with feature data
|
||||||
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
||||||
passing: features?.done.length ?? 0,
|
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,
|
percentage: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface WebSocketState {
|
|||||||
progress: {
|
progress: {
|
||||||
passing: number
|
passing: number
|
||||||
in_progress: number
|
in_progress: number
|
||||||
|
needs_human_input: number
|
||||||
total: number
|
total: number
|
||||||
percentage: 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) {
|
export function useProjectWebSocket(projectName: string | null) {
|
||||||
const [state, setState] = useState<WebSocketState>({
|
const [state, setState] = useState<WebSocketState>({
|
||||||
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',
|
agentStatus: 'loading',
|
||||||
logs: [],
|
logs: [],
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -107,6 +108,7 @@ export function useProjectWebSocket(projectName: string | null) {
|
|||||||
progress: {
|
progress: {
|
||||||
passing: message.passing,
|
passing: message.passing,
|
||||||
in_progress: message.in_progress,
|
in_progress: message.in_progress,
|
||||||
|
needs_human_input: message.needs_human_input ?? 0,
|
||||||
total: message.total,
|
total: message.total,
|
||||||
percentage: message.percentage,
|
percentage: message.percentage,
|
||||||
},
|
},
|
||||||
@@ -385,7 +387,7 @@ export function useProjectWebSocket(projectName: string | null) {
|
|||||||
// Reset state when project changes to clear stale data
|
// Reset state when project changes to clear stale data
|
||||||
// Use 'loading' for agentStatus to show loading indicator until WebSocket provides actual status
|
// Use 'loading' for agentStatus to show loading indicator until WebSocket provides actual status
|
||||||
setState({
|
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',
|
agentStatus: 'loading',
|
||||||
logs: [],
|
logs: [],
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user