feat(tui): add wizard-style recipe runner#14
Conversation
JC admins living in the TUI had no way to run recipes without dropping to CLI and guessing --param flag names. This PR adds recipes as a first-class TUI experience: list → wizard-style parameter form → live execution with per-step progress. - New "Workflows" category and virtual "recipes" entry on the home grid - RecipeListScreen: merged built-in + user catalog, filterable, with source/tag/param/step annotations per row - RecipeParamFormScreen: one input per parameter with defaults, required markers, type-aware validation (int/bool), step preview, plan toggle - RecipeRunScreen: streams engine progress via io.Pipe into line-scanned status transitions (pending → running → done|skipped|failed), renders on_success/on_failure messages, supports plan-mode preview - cmd/tui.go wires the CLI dispatcher and tea.Program into the screen package via small hooks to avoid a cmd ↔ tui import cycle Reuses recipe.LoadAll, ResolveParams, Plan, RenderPlanHuman, Execute, and NewDispatcher verbatim — no changes to the recipe engine. Follow-ups (not in this PR): - jc software update --file flag for binary swaps (KLA-379 follow-up) - context.Context on recipe.Execute for mid-step cancellation - RecipeAuthorScreen for creating/editing recipes from the TUI Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Running a report-style recipe (stale-device-cleanup, compliance-report, audit-*, etc.) from the TUI was hiding the actual data — only the "Stale device scan complete. Review devices..." completion message rendered, which is useless when the devices themselves aren't visible. - Render each step's captured stdout inline below its status line on done - Viewport-based scrolling (j/k/up/down, g/G, PgUp/PgDn, space) because altscreen mode disables terminal scrollback - Scroll indicator shows position + range when content overflows - Skipped steps omit the empty-output block Also renames RecipeParamFormScreen.Title() from "Run: X" to "Configure: X" so the breadcrumb no longer reads `... > Run: X > Run: X`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed 61dae4e: recipe output now renders inline below each step with altscreen-compatible scrolling (j/k, g/G, PgUp/PgDn). Fixes the 'stale-device-cleanup complete — but where are the devices?' gap reported during local testing. Also renames param screen to 'Configure: X' to fix duplicate breadcrumb ( |
- recipe_list.go: drop unreachable \`case "q":\` handler (app-level GlobalKeyMap.Quit intercepts single-key "q" before screens see it, so pressing "q" on the list quit the whole TUI instead of navigating back) - recipe_list.go: collapse double s.filter.Focus() call in "/" handler that silently discarded the first returned Cmd - recipe_run.go: hoist bufio.Reader to a screen field so buffered data isn't dropped between readNextLine invocations (today benign because io.Pipe guarantees one Read per Write, but an anti-pattern) - recipe_run.go: remove dead \`recipeProgressMsg\` struct (leftover from earlier design, never instantiated) Regression tests added for the "q"-does-not-pop and single-Focus-on-"/" behaviors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed 659ea5f addressing the 4 Bugbot findings:
Regression tests added for the q-no-pop and single-Focus behaviors. Note: the |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Reviewed by Cursor Bugbot for commit 659ea5f. Configure here.
| result, err := s.recipe.Execute(RecipeDispatcher, s.params, s.pipeW) | ||
| _ = s.pipeW.Close() | ||
| teaProgramSend(recipeDoneMsg{result: result, err: err}) | ||
| }() |
There was a problem hiding this comment.
Stale recipeDoneMsg corrupts subsequent recipe run screen
Medium Severity
The background goroutine sends recipeDoneMsg via the global teaProgramSend without any screen identity. If the user presses Esc during execution and then starts another recipe, the first goroutine's recipeDoneMsg is routed to the new RecipeRunScreen. The handler prematurely sets done = true, populates result with stale data, and closes the new screen's pipeR, killing progress monitoring for the active execution.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 659ea5f. Configure here.
| userNames := make(map[string]bool, len(userDir)) | ||
| for _, r := range userDir { | ||
| userNames[r.Name] = true | ||
| } |
There was a problem hiding this comment.
Non-mockable filesystem call in loadRecipes bypasses test loaders
Low Severity
loadRecipes calls recipe.LoadFromDir(recipe.RecipesDir()) directly for user-recipe identification, bypassing the overridable recipeLoader/builtInLoader hooks. This makes TestRecipeListScreen_SourceIdentification depend on real filesystem state — if ~/.config/jc/recipes/ contains a file named builtin-one.yaml, the test would fail because userNames["builtin-one"] becomes true.
Reviewed by Cursor Bugbot for commit 659ea5f. Configure here.


Summary
internal/tui/screen/recipe.LoadAll,ResolveParams,Plan,Execute, andNewDispatcherverbatim — no engine changesWhy
JC admins living in the TUI had no way to run recipes without dropping to CLI and guessing
--paramflag names. Built-in recipes likeonboard-userandsecurity-auditare exactly the kind of thing that benefits from an interactive form.Scope
In this PR (runner):
RecipeListScreen— merged built-in + user catalog, filterable, source/tag/param/step annotations per rowRecipeParamFormScreen— one input per parameter with defaults, required markers (*), type-aware validation (int/bool), step preview, plan-mode toggleRecipeRunScreen— streams engine progress viaio.Pipeinto line-scanned status transitions (pending → running → done|skipped|failed), renderson_success/on_failurehook messages, supports plan-mode previewFollow-up (separate PR):
RecipeAuthorScreen— create/edit recipes from the TUI (the "B" half of the originally scoped feature; splitting keeps this PR reviewable and lets real use of the runner inform the author UX)context.Contextonrecipe.Executefor mid-step cancellation (current behavior: Esc leaves the screen cleanly, in-flight step completes in the background)Design notes
cmd/tui.gowires the CLI dispatcher andtea.Programinto the screen package via two small hooks (screen.RecipeDispatcherandscreen.RegisterTeaProgram) to avoid a cmd ↔ tui import cycle — same pattern as the existingapp.NewHelpScreenhook[N/M] name... done|failed|skipped) is stable engine output;recipe_run.gohas a focused regex test so a format change fails loudly rather than silently breaking the UIio.Pipefor responsive UI. No changes to the engineTest plan
go test ./...— all pass, including 19 new testsgo vet ./...— cleanrecipesvirtual entry andCategoryWorkflowsorderingjc recipe list,jc recipe run security-audit --planstill work unchangedjc tui→ navigate to Recipes → runsecurity-audit(no params) → verify output matchesjc recipe run security-auditonboard-userwith a throwaway username, verify required-field validation blocks submit when empty🤖 Generated with Claude Code
Note
Medium Risk
Introduces new async execution and global hooks (
RecipeDispatcher, programSend) inside the TUI, which could impact UI responsiveness or lifecycle if miswired, but it doesn’t alter the underlying recipe engine or API clients.Overview
Adds a new Recipes workflow to the TUI: a virtual
recipeshome entry (under a new Workflows category) that opens a recipe catalog, collects parameters in a wizard form (with required/default/type validation and optional plan mode), and runs recipes with a live step-by-step progress/output view.Wires recipe execution into the TUI bootstrap by injecting a
recipedispatcher and registering the runningtea.Programso the async recipe runner can post completion messages back into the UI loop. Includes new/updated tests to cover registry changes and the new screens’ navigation, filtering, validation, and progress parsing.Reviewed by Cursor Bugbot for commit 659ea5f. Bugbot is set up for automated code reviews on this repo. Configure here.