- Code-build (and any
kind=newproject) silently auto-picked a template (nextjsvsnextjs-app) from a single free-model call. The user never saw the choice, let alone got to confirm or override it. - The frontend understood only three hard-coded
ask_userschema shapes (enum buttons / approve-reject / free text), each rendered by a bespoke panel and dispatched by duplicated detection logic. - Multi-select and “pick an option or type something else” were not expressible at all.
- The user confirms or redirects the path when it matters (low-confidence template pick), and is never interrupted when it doesn’t (high-confidence pick auto-proceeds).
- Any new option-pick / clarification / multi-select question is a YAML-or-schema change, not new UI.
- The in-run reply box and the main task composer share one input control.
What’s changed
The feedback contract (new)
-
worker/worker/graph/feedback.py—FeedbackRequest/FeedbackOption/FeedbackResponsepydantic models,build_feedback_schema(...),read_feedback_request(...), andparse_feedback_response(schema, value). The request rides verbatim in the existingask_userpayload’sschemafield as a JSON-Schema object carrying anx-feedbackvendor extension:Each option may carry optional visual media —swatches(a list of CSS color strings rendered as a row of chips) andimage_url(a preview/screenshot URL rendered as a thumbnail). Both default empty and are inert to validation, so an option without them renders as a plain text choice exactly as before. The normalized reply is{"selected": [...], "text": "..."}; a bare string or{"answer": "..."}is still accepted for single-select (legacy enum panel back-compat). -
apps/frontend/lib/types/feedback.ts— the TS mirror +readFeedbackRequest(schema)returningnullwhenx-feedbackis absent (so legacy detectors still fire).readOptionmaps the wireimage_url(snake_case) toimageUrland filtersswatchesto strings.
Visual media on options (new)
Options can now show what they look like, not just describe it — genericswatches / image_url fields plumbed through the existing
builders (no per-choice-type special-casing):
- Design / color themes → color swatches + preview.
DesignSpecalready carries summarizedtokens(cssVars) and apreviewstring.worker/worker/orchestration/design_selector.pythreadstokens+previewonto each candidate dict;_design_confirm_optionsinworker/worker/main.pyderivesswatchesfrom the color tokens only (background,foreground,primary,accent—radius/font-*excluded) and setsimage_urlfrompreview. Token values pass through verbatim (#rrggbb,oklch(...),hsl(...)) and are used as raw CSS colors by the chips. Zero new data required. - Templates → design screenshots.
TemplateSpecgains apreview_urlfield, parsed from an optionalpreview_url:key in eachtemplate.yaml(worker/worker/runtime/template_registry.py).worker/worker/orchestration/template_selector.pysets each candidate’simage_urlfrom it;_park_for_template_confirmationspreads candidates straight intoFeedbackOption, so the screenshot flows through unchanged. Templates without apreview_urlkeepimage_url=""and render text-only.
Worker
worker/worker/graph/scenarios/runners.py—HumanApprovalLevelRunnergeneralized with afeedback_modeswitch (approvaldefault |select|input). On resume it validates viaparse_feedback_responseand folds permetadata.assign(intent_route→ scenario chaining; any other key → that state key)._unwrap_picknow also reads the{selected: [...]}shape.worker/worker/graph/scenarios/schemas.py— newhuman_feedbackLevelKind alias (shares the runner).worker/worker/graph/scenarios/orchestrator.py— registers the alias.worker/worker/orchestration/template_selector.py—SelectionResultnow carriesconfidence(model self-reported, clamped 0–1, defaults to 0.0 → fail toward asking) andcandidates(the enabled templates).worker/worker/main.py—_maybe_select_templateis confidence-gated: at/abovetemplate_confirm_thresholdit auto-picks; below it (and only when >1 candidate) it parks the job pre-graph via_park_for_template_confirmation— emits anask_userwith thex-feedbackoptions (model pick markedrecommended), stashes the serializedJobEnvelope+ apending_kind="pre_graph_template"marker on the job’s Redis hash, and setsstatus=awaiting_user._resume_pre_graph_templatepins the chosen template (or the recommended pick when the user only typed a note), threads any free-text note into the prompt, clears the markers, and re-enqueues — reusing the normal job path.worker/worker/config.py—template_confirm_threshold(TEMPLATE_CONFIRM_THRESHOLD, default0.0= ship dark / never pause; set0.7to mirror the intent-classifier gate).worker/worker/graph/scenarios/prompts.py—intent_classifier_disambiguatenow dual-emits thex-feedbackblock alongside the legacyanswer.enumkeys (drop the legacy keys once every client renders the universal panel).
Frontend
apps/frontend/components/layout/footer/universal-feedback-panel.tsx(new) —UniversalFeedbackPanelrenders anyFeedbackRequest: buttons (single) / checkboxes (multi, honoring min/max) / a free-text area (optional/required), with therecommendedoption pre-selected. Submits{selected, text}. Renders an option’sswatchesas a row of color chips and itsimage_urlvia a localOptionThumb(skeleton shimmer → fade-in, collapses on load error — the same pattern as the deployment-card preview).apps/frontend/components/input/composer-textarea.tsx(new) — the shared textarea primitive (⌘/Ctrl+Enter submit) used by both the feedback panel and the main task composer.event-timeline.tsx(ClarificationPrompt) andtask-output-modal.tsx— routex-feedbackto the universal panel first, falling back to the legacy enum/approval/text detectors. The bespokeAskUserEnumPanel/AskUserTextPanelremain as adapters.
Impact scope
- Backwards-compatible. No event or transport rename — the contract
rides the existing
ask_userschemafield. Legacy enum / approval / free-text runs render unchanged. The intent-classifier disambiguation and security-review approval gate still resume correctly. Theswatches/image_urloption fields are additive and default empty: options that omit them render exactly as text-only choices. - Defaults to off.
template_confirm_threshold=0.0means template selection auto-picks exactly as before; raising the threshold turns on the pause. Thehuman_feedbackLevelKind andfeedback_modeare opt-in per level. - Affects: worker scenario runners + template selection + main loop,
the FE ask_user rendering path. The pre-graph park leaves the lane
assigned (same as an in-graph
human_approvalpause).
Tests
tests/test_feedback_contract.py— build/parse round-trips for all four shapes; validation of min/max-select, required free-text, and unknown option ids;swatches/image_urlround-trip + empty defaults.tests/test_design_gate.py—_design_confirm_optionssurfaces the color palette (color tokens only, canonical order) + preview asimage_url, and stays strict (nomode/tokens/previewleak).tests/test_template_registry.py—preview_urlparsed (present / absent →""/ non-string coerced).tests/test_template_selector.py— confidence parse/clamp; fallback picks carry0.0; candidates always surfaced.tests/test_template_confirm_gate.py— threshold0.0never parks; low confidence parks with thex-feedbackoptions; resume pins the chosen template (or recommended) and re-enqueues with the note.tests/test_feedback_gate_runner.py—feedback_modefolds (state key +intent_routechaining + bare-string legacy + invalid-reply no-crash); approval verdict regression.- Frontend:
npx --no-install tsc --noEmitandbiome checkclean on the changed files.