refactor(ui): action-first flow with immediate save#2340
Merged
Conversation
Restructure the `terramate ui` interactive flow from an upfront environment selection to an action-first pattern where users pick what to do before being asked contextual questions. ## Flow Changes **Before:** Env → Action → Collection → Bundle → Inputs **After:** Action → Bundle (flat list) → [Env if needed] → Inputs - Remove mandatory upfront environment selection screen - Flatten Create from two-step Collection→Bundle picker into a single sorted list of all bundle variants across all sources - Defer environment selection to after bundle pick, only when the bundle requires it (environments.required = true) - Reconfigure/Promote show all items by default with an env filter toggle (press 'e') instead of gating behind env selection - Promote computes promotable bundles across all environments ## UI Improvements - Add sticky detail box with titled border showing bundle metadata (name, version, class, collection, source, path, env flow) - Add dismissible error dialog overlay for bundle-not-enabled and other errors (centered modal, red border, any-key dismiss) - Simplify list rows: Create shows name+version per line, Reconfigure/Promote use grouped layout with compact instance rows - Update breadcrumbs: root="Terramate Catalyst", last segment bold, current action only (e.g. "Create MyBundle (staging)") - Add ↑↓ navigation hints in help text - Correct Esc navigation chain: Inputs → Env Select → Bundle List → Overview ## Env Filter (Reconfigure & Promote) - Precomputed filter states: only envs with matching bundles are in the cycle, plus "env-less" for Reconfigure - Press 'e' to cycle through filters, Esc to reset to "all" - Breadcrumb shows active filter (e.g. "in staging", "to production") - Help text previews the next filter env ## Technical Details - New ViewCreateEnvSelect state for deferred env picker - New flatBundleEntry type mapping flat rows to collection/bundle indices - New envFilterState type for precomputed filter cycles - New renderDetailBox with labeled fields, truncation, and separators - loadBundleEvalContext now takes explicit env parameter - buildAllPromoteBundles returns parallel bundle+targetEnv slices - createBackView() helper for correct back-navigation
Create: add bundle description with indent, bold names, remove excess padding, use separator between entries. Reconfigure/Promote: flat per-instance scrolling (fixes groups with many instances not scrolling), empty line between groups, parameterized scrollWindowVar separator. Reconfigure: always show Environment field (n/a for env-less) to prevent detail box height flapping.
Fix gofmt alignment in ViewState constants. Remove unused 'base' parameter from overlayErrorDialog.
Rename "Terramate Catalyst" to "Terramate UI" in breadcrumbs. Align env flow labels (envA → envB) in Promote list by padding alias names to the max width across all instances, so the arrow columns line up regardless of alias length.
c: create, r: reconfigure, p: promote, q: quit Shortcuts work when focus is on the commands section. Help text shows available shortcuts at the bottom.
Pass innerWidth instead of contentWidth to renderDetailBox so the detail box fills the full width inside the outer border. Previously the scrollbar gutter was subtracted twice, making the detail box narrower than the panel and causing uneven right-side spacing.
startNestedCreate now checks if the nested bundle needs env selection after loadBundleDef, redirecting to ViewCreateEnvSelect when environments are configured. Previously it unconditionally set ViewCreateInput, leaving the input form uninitialized when loadBundleDef deferred evaluation for env-requiring bundles.
Nested bundle creates (via bundle-ref inputs) now inherit the parent's environment instead of re-asking. The deferred env selection path in loadBundleDef is skipped when createStack is non-empty, since nested bundles must live in the same env scope as their parent.
Nested creates now show the full parent chain in the breadcrumb: Create VPC (staging) / Create Subnet / Create SecurityGroup instead of the previous barely-visible "(for VPC)" suffix. Supports arbitrary nesting depth via createStack traversal.
Show env in [] only on the current (last) breadcrumb level. Remove the right-aligned env label from the header — it's redundant now that env context is in the breadcrumb.
CreateFrame now saves and restores selectedCollIdx, selectedBundleIdx, selectedBundleDefEntry, and selectedBundleSource alongside flatBundleCursor and inputsForm. Previously only the cursor and form were saved, so after a nested create and restore, the bundle definition still pointed to the nested bundle instead of the parent.
Create no longer batches changes as "pending" — bundles are saved to disk immediately on confirm, then the registry is reloaded. This fixes nested bundle references: the parent can now resolve bundle() calls to dependencies created in the same session. - All Create confirmations save + reload immediately - NewCreateChange rebinds bundle()/bundles() functions to current registry so references to just-saved bundles resolve correctly - Confirm button renamed to "Save" - No more pending changes accumulating from Create actions
BundleRefWidget.FormatDisplay now handles both string values (alias) and resolved bundle objects (extracts alias attribute). Also falls back to wctx.Value when w.value is NilVal (FormatDisplay called before Prepare), and handles unknown/dynamic values gracefully. Previously, reconfigure showed "<not set>" for bundle-ref inputs because the stored value was a resolved object, not a raw string.
Bundle-ref inputs are stored as resolved objects (from tm_bundle) but must be written as alias strings in YAML and handled correctly during type validation. - BundleType.Apply: accept resolved objects, extract alias string - generateBundleYAML: unwrap bundle-typed values to aliases using def.Type check before YAML serialization - BundleRefWidget.FormatDisplay: handle object values, extract alias for display (fixes "<not set>" in reconfigure) - Sort groups alphabetically by name, instances by alias - Sort promote bundles in grouped display order to fix cursor mismatch
…n promote When promoting a bundle that references another bundle not yet promoted to the target environment, show a clear message: "A referenced bundle has not been promoted to this environment yet. Promote dependencies first."
- Keyboard shortcuts only match when focus is on commands (move focus check into case condition to avoid commandIdx side-effects) - Bounds check flatBundleCursor before accessing flatBundles - Use rune-based truncation for correct multi-byte character handling - Improve wrapMissingBundleRefError to match "This value is null" (the actual HCL error text) instead of generic "null"
- Remove view_env_select.go (unreachable — no code path sets ViewEnvSelect) - Remove ViewEnvSelect dispatch cases from Update/View - Remove unused envCursor field from Model - Inline nextViewAfterCloudLogin (trivial one-liner) and remove function
Use lipgloss.Width() instead of len() for label padding calculation to correctly handle multi-byte characters.
The parameter was used for right-aligning the env label which was removed earlier. All 10 callers updated.
loadBundleDef can fail for multiple reasons (resolve, load, parse, enablement). Using "Bundle is not enabled" as the title for all failures was misleading. Now uses the generic "Error" title. Removed unused updateErrorWithTitle function.
…c order Groups with the same name but different version/source now sort deterministically by using the detail string (version+source) as a secondary sort key.
Previously, Ctrl+C with an error dialog visible would set ctrlCPending but the dialog stayed. The second Ctrl+C would dismiss the dialog instead of exiting. Now Ctrl+C always clears the dialog first, so the double-press exit works regardless of dialog state.
7f3d5be to
e676594
Compare
|
Preview of macos-15/go tests in 0a8f97f🔍 View Details on Terramate Cloud .
commands/ui
|
c8c0dbf to
2c1328a
Compare
2c1328a to
f72d46f
Compare
f72d46f to
4099cf0
Compare
- Reconfigure and Promote now save immediately on confirm (like Create) - Remove Edit flow (view_edit.go deleted) — no pending changes to edit - Remove PendingChanges/ProposedChanges infrastructure from Registry - Replace pending summary panel with "Recently Changed" session panel showing grouped bundles with change kind tags (created/reconfigured/promoted) - Session panel allows selecting a bundle to reconfigure it again - Reconfig/promote input form: single unified list (no Changed/Unchanged split) - Immutable inputs: blue values, "immutable" tag, cursor skips them - Changed values shown in orange with inline diff (new <- old) - Buttons at bottom: Save/Cancel (or Back when no changes in reconfig) - ESC with unsaved reconfig changes triggers discard confirmation - After editing, cursor returns to the just-edited input - Rebind bundle()/bundles() in NewReconfigChange and NewPromoteChange - Remove dead code: SavedChange, NewChangeFromExisting, MarkedForReplacement, ProposalID, reEditing, hasEdited, skipDiscardConfirm, cancelLabel, savedCompletedCursor, completedVisibleIndices, completedCursorToLine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4099cf0 to
d9152b1
Compare
snakster
reviewed
Apr 13, 2026
- [Without Environment] tag uses same blue color as other env tags - Left/right arrow nav cycles between Save/Cancel buttons - Last saved bundle highlighted with bold + "saved" tag (clears on keypress) - Command hotkeys use underlined letter instead of [c] suffix - Hotkeys accept both upper and lowercase input Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove ui.Registry wrapper — use *config.Registry directly - MatchingBundleOptions and IsBundleUnique are now standalone functions - Replace context.TODO() with est.Context in all change constructors - Remove double-dereference (est.Registry.Registry → est.Registry) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove object→alias extraction from BundleType.Apply (typeschema layer) - Add normalizeBundleRefValues() in UI layer to extract aliases before values enter the form - Replace brittle string-matching wrapMissingBundleRefError with checkBundleRefsResolved() that checks for null bundle-ref values after evaluation — produces a clear error with the input name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hoist currentReconfigFilter() call outside the per-bundle loop - Only switch to InputFocusCompleted after confirm when all inputs are still filled (clearDependents may create unfilled inputs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… all flows - Add bounds check on LocalBundleDefs[bundleIdx] access to prevent potential out-of-bounds panic if collections and local defs diverge - Add checkBundleRefsResolved to NewCreateChange and NewReconfigChange (was only in NewPromoteChange) for consistent bundle-ref validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reconfig discard: goes to ViewReconfigSelect (or ViewOverview if entered from session panel) - Promote discard: goes to ViewPromoteSelect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2f7199e. Configure here.
After reloadAll(), parent form widgets hold the old *config.Registry pointer via SharedWidgetContext. Replace content in-place (*est.Registry = *newReg) so all existing pointers see the updated data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Replaces #2335 (closed). Combines the action-first UI restructure with immediate save and UX improvements.
From #2335 (base changes)
New in this PR
ui.Registrywrapper, use*config.RegistrydirectlyBundleType.Applyto UI layercheckBundleRefsResolved()replaces brittle string-matching error detectionTest plan
🤖 Generated with Claude Code
Note
Medium Risk
Changes core TUI workflow to write bundle instance YAML immediately (create/reconfigure/promote) and reload the registry in-session, so mistakes can persist to disk and affect subsequent resolution/filtering. UI navigation/rendering was substantially reworked, increasing risk of UX regressions across create/reconfig/promote flows.
Overview
Refactors the TUI to an action-first flow and removes the pending/batch save model: Create/Reconfigure/Promote now save bundle instance YAML immediately, reload the
config.Registryafter each save, and the overview shows a “Recently Changed” session panel that can jump straight into reconfigure.Create selection is redesigned into a flat, sorted bundle list with a detail box, and Create adds a deferred environment picker when the chosen bundle requires an environment. Reconfigure/Promote selection adds environment-based filtering (cycle with
e), richer detail boxes, and updated breadcrumbs/hotkeys.InputsFormis unified into a single ordered list (no Changed/Unchanged split) with inline diffs, improved cursor/navigation (skips immutable/disabled inputs, wraps), and updated button logic (Save/Cancel; Back when no reconfig changes; ESC discard confirmation for reconfig). Bundle-ref handling is hardened by normalizing stored object values back to alias strings, rebindingbundle()/bundles()to the live registry during change evaluation, and addingcheckBundleRefsResolved()for clearer missing-dependency errors.Reviewed by Cursor Bugbot for commit 0a8f97f. Bugbot is set up for automated code reviews on this repo. Configure here.