Skip to content

feat(tui): add wizard-style recipe runner#14

Merged
jklaassenjc merged 3 commits into
mainfrom
juergen/tui-recipes-runner
Apr 22, 2026
Merged

feat(tui): add wizard-style recipe runner#14
jklaassenjc merged 3 commits into
mainfrom
juergen/tui-recipes-runner

Conversation

@jklaassenjc
Copy link
Copy Markdown
Collaborator

@jklaassenjc jklaassenjc commented Apr 22, 2026

Summary

  • 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
  • Three new screens (list, param form, run) all in internal/tui/screen/
  • Reuses recipe.LoadAll, ResolveParams, Plan, Execute, and NewDispatcher verbatim — no engine changes

Why

JC admins living in the TUI had no way to run recipes without dropping to CLI and guessing --param flag names. Built-in recipes like onboard-user and security-audit are 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 row
  • RecipeParamFormScreen — one input per parameter with defaults, required markers (*), type-aware validation (int/bool), step preview, plan-mode toggle
  • RecipeRunScreen — streams engine progress via io.Pipe into line-scanned status transitions (pending → running → done|skipped|failed), renders on_success/on_failure hook messages, supports plan-mode preview

Follow-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.Context on recipe.Execute for mid-step cancellation (current behavior: Esc leaves the screen cleanly, in-flight step completes in the background)

Design notes

  • cmd/tui.go wires the CLI dispatcher and tea.Program into the screen package via two small hooks (screen.RecipeDispatcher and screen.RegisterTeaProgram) to avoid a cmd ↔ tui import cycle — same pattern as the existing app.NewHelpScreen hook
  • Progress line format ([N/M] name... done|failed|skipped) is stable engine output; recipe_run.go has a focused regex test so a format change fails loudly rather than silently breaking the UI
  • The recipe engine is synchronous; the run screen wraps it in a goroutine + io.Pipe for responsive UI. No changes to the engine

Test plan

  • go test ./... — all pass, including 19 new tests
  • go vet ./... — clean
  • Unit tests for list/param-form/run screens (title, render, navigation, validation, state transitions)
  • Registry test for the new recipes virtual entry and CategoryWorkflows ordering
  • CLI smoke: jc recipe list, jc recipe run security-audit --plan still work unchanged
  • Interactive TUI smoke (requires a real terminal): launch jc tui → navigate to Recipes → run security-audit (no params) → verify output matches jc recipe run security-audit
  • Interactive TUI smoke: run onboard-user with 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, program Send) 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 recipes home 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 recipe dispatcher and registering the running tea.Program so 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.

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>
Comment thread internal/tui/screen/recipe_run.go
Comment thread internal/tui/screen/recipe_run.go Outdated
Comment thread internal/tui/screen/recipe_list.go
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>
@jklaassenjc
Copy link
Copy Markdown
Collaborator Author

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 (Run: X > Run: X).

Comment thread internal/tui/screen/recipe_list.go
- 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>
@jklaassenjc
Copy link
Copy Markdown
Collaborator Author

Pushed 659ea5f addressing the 4 Bugbot findings:

  • Medium: removed unreachable case "q": in RecipeListScreen (app-level Quit intercepts it)
  • Low: collapsed double s.filter.Focus() call in / handler
  • Low: hoisted bufio.Reader to a screen field so buffered data survives across readNextLine calls
  • Low: removed dead recipeProgressMsg type

Regression tests added for the q-no-pop and single-Focus behaviors.

Note: the claude-review check is failing on both this PR and #15 with "Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN is required" — looks like a missing repo-level Actions secret, not a code issue.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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})
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 659ea5f. Configure here.

userNames := make(map[string]bool, len(userDir))
for _, r := range userDir {
userNames[r.Name] = true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 659ea5f. Configure here.

@jklaassenjc jklaassenjc merged commit 5ec6119 into main Apr 22, 2026
5 of 6 checks passed
@jklaassenjc jklaassenjc deleted the juergen/tui-recipes-runner branch April 24, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants