Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d80d90b
refactor(ui): restructure terramate ui to action-first flow
mariux Apr 8, 2026
03f0c4e
refactor(ui): improve list rendering across all views
mariux Apr 8, 2026
d57c9c3
refactor(ui): lint cleanup — gofmt alignment and unused parameter
mariux Apr 8, 2026
0c6cf1b
refactor(ui): rename breadcrumb root, align promote env flow
mariux Apr 9, 2026
48ebfa5
refactor(ui): add keyboard shortcuts for home screen actions
mariux Apr 9, 2026
54c1b4e
refactor(ui): fix detail box width to match outer panel border
mariux Apr 9, 2026
a3b6247
refactor(ui): handle deferred env selection in startNestedCreate
mariux Apr 11, 2026
ae2682a
refactor(ui): nested bundles inherit parent env, skip env picker
mariux Apr 11, 2026
67ec5df
refactor(ui): show full nesting chain in create breadcrumb
mariux Apr 11, 2026
ca30512
refactor(ui): env in brackets on current level, remove right-side label
mariux Apr 11, 2026
d5f2f11
refactor(ui): color env tag in breadcrumb — blue for env, amber for e…
mariux Apr 11, 2026
3c93b01
refactor(ui): save/restore bundle def state in CreateFrame
mariux Apr 11, 2026
1612929
feat(ui): save bundle immediately on Create confirm
mariux Apr 11, 2026
b0f02a5
fix: handle resolved bundle objects in BundleRefWidget.FormatDisplay
mariux Apr 11, 2026
c5c972f
fix: handle resolved bundle objects in promote/reconfigure
mariux Apr 11, 2026
9d584c4
refactor(ui): improve error message for missing bundle dependencies o…
mariux Apr 11, 2026
d1ee9f6
refactor(ui): red-team review fixes
mariux Apr 11, 2026
08d6a8b
refactor(ui): remove dead code from review findings
mariux Apr 11, 2026
c541436
refactor(ui): use visual width for label padding in detail box
mariux Apr 11, 2026
8614bc0
refactor(ui): remove unused panelWidth parameter from renderHeader
mariux Apr 11, 2026
ee7b6f8
refactor(ui): use generic error title for loadBundleDef failures
mariux Apr 11, 2026
25604b4
refactor(ui): add detail as tiebreaker in group sort for deterministi…
mariux Apr 12, 2026
9f48ad8
refactor(ui): gofmt fix
mariux Apr 12, 2026
4275ec4
refactor(ui): clear error dialog on Ctrl+C so exit is not blocked
mariux Apr 12, 2026
d9152b1
refactor(ui): remove pending changes, save all actions immediately
mariux Apr 12, 2026
8b130d5
refactor(ui): address slack feedback on UX polish
mariux Apr 13, 2026
ad6e06a
refactor(ui): remove Registry wrapper, use est.Context
mariux Apr 13, 2026
3321fb3
refactor(ui): move bundle-ref logic out of type system, check upfront
mariux Apr 13, 2026
8671da6
fix(ui): hoist filter call, guard confirmCurrent focus switch
mariux Apr 13, 2026
7d51fee
fix(ui): bounds-check LocalBundleDefs, add checkBundleRefsResolved to…
mariux Apr 13, 2026
2f7199e
fix(ui): discard navigates back to entry point, not overview
mariux Apr 13, 2026
0a8f97f
fix(ui): update registry in-place to avoid stale pointers
mariux Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 78 additions & 60 deletions commands/ui/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import (
"github.com/terramate-io/terramate/hcl"
"github.com/terramate-io/terramate/hcl/ast"
"github.com/terramate-io/terramate/project"
"github.com/terramate-io/terramate/stdlib"
"github.com/terramate-io/terramate/typeschema"
"github.com/terramate-io/terramate/yaml"
)

// ChangeKind indicates the type of pending change.
// ChangeKind indicates the type of change.
type ChangeKind string

// ChangeCreate and the following constants enumerate the supported change kinds.
Expand All @@ -37,7 +38,7 @@ const (
ChangePromote ChangeKind = "change_promote"
)

// Change represents a pending change in the summary.
// Change represents a bundle change (create, reconfigure, or promote).
type Change struct {
Kind ChangeKind
HostPath string
Expand All @@ -62,21 +63,6 @@ type Change struct {

Warnings []string // Non-fatal warnings surfaced after resolving the change

// Simple incremental ID returned to the LLM so it can track this change reliably.
ProposalID int

// Set to exclude from uniqueness checks, so an edited change doesn't conflict with itself.
MarkedForReplacement bool
}

// SavedChange is a lightweight summary of a change that was persisted to disk.
type SavedChange struct {
Kind ChangeKind
Name string
EnvID string
FromEnvID string
ProjectPath string
HostPath string
}

// NewCreateChange builds a Change that represents a new bundle creation.
Expand All @@ -90,6 +76,12 @@ func NewCreateChange(
) (Change, error) {
schemactx = schemactx.ChildContext()

// Rebind bundle() functions to the current registry so that references
// to bundles created during this session (e.g. nested bundles that were
// saved immediately) are resolvable.
schemactx.Evalctx.SetFunction(stdlib.Name("bundle"), config.BundleFunc(est.Context, est.Registry, activeEnv, false))
schemactx.Evalctx.SetFunction(stdlib.Name("bundles"), config.BundlesFunc(est.Registry, activeEnv))

Comment thread
cursor[bot] marked this conversation as resolved.
// The form may or may not contain values for all defaults.
// In this step we re-run input evaluation like it would be done if this was a bundle instance that
// has the form values as inputs. This will ensure we get all the inputs.
Expand All @@ -102,6 +94,9 @@ func NewCreateChange(
if err != nil {
return Change{}, err
}
if err := checkBundleRefsResolved(inputDefs, allValues); err != nil {
return Change{}, err
}

// We check if the bundle has an explicit alias and add it to the context if yes.
alias, err := setupExplicitBundleAlias(schemactx.Evalctx, bde.Define)
Expand Down Expand Up @@ -156,7 +151,7 @@ func NewCreateChange(
}

// Final check: Is the bundle unique?
if err := est.Registry.IsBundleUnique(alias, bde.Metadata.Class, hostPath, env); err != nil {
if err := IsBundleUnique(est.Registry, alias, bde.Metadata.Class, hostPath, env); err != nil {
return Change{}, err
}

Expand Down Expand Up @@ -188,6 +183,11 @@ func NewReconfigChange(
) (Change, error) {
schemactx = schemactx.ChildContext()

// Rebind bundle() functions to the current registry so that references
// to bundles created/reconfigured during this session are resolvable.
schemactx.Evalctx.SetFunction(stdlib.Name("bundle"), config.BundleFunc(est.Context, est.Registry, bundle.Environment, false))
schemactx.Evalctx.SetFunction(stdlib.Name("bundles"), config.BundlesFunc(est.Registry, bundle.Environment))

hostPath := bundle.Info.HostPath()
projPath := project.PrjAbsPath(est.Root.HostDir(), hostPath).String()

Expand All @@ -203,6 +203,9 @@ func NewReconfigChange(
if err != nil {
return Change{}, err
}
if err := checkBundleRefsResolved(inputDefs, allValues); err != nil {
return Change{}, err
}

// This will only be set if there is an explicit alias.
newAlias, err := setupExplicitBundleAlias(schemactx.Evalctx, bde.Define)
Expand Down Expand Up @@ -258,6 +261,11 @@ func NewPromoteChange(
) (Change, error) {
schemactx = schemactx.ChildContext()

// Rebind bundle() functions to the current registry so that references
Comment thread
snakster marked this conversation as resolved.
// to bundles promoted during this session are resolvable.
schemactx.Evalctx.SetFunction(stdlib.Name("bundle"), config.BundleFunc(est.Context, est.Registry, env, false))
schemactx.Evalctx.SetFunction(stdlib.Name("bundles"), config.BundlesFunc(est.Registry, env))

hostPath := bundle.Info.HostPath()
projPath := project.PrjAbsPath(est.Root.HostDir(), hostPath).String()

Expand All @@ -273,6 +281,9 @@ func NewPromoteChange(
if err != nil {
return Change{}, err
}
if err := checkBundleRefsResolved(inputDefs, allValues); err != nil {
return Change{}, err
}

// This will only be set if there is an explicit alias.
newAlias, err := setupExplicitBundleAlias(schemactx.Evalctx, bde.Define)
Expand Down Expand Up @@ -317,48 +328,6 @@ func NewPromoteChange(
}, nil
}

// NewChangeFromExisting rebuilds a Change from a previously created change with updated values.
func NewChangeFromExisting(
est *EngineState,
oldChange Change,
schemactx typeschema.EvalContext,
inputDefs []*config.InputDefinition,
values map[string]cty.Value,
) (Change, error) {
switch oldChange.Kind {
case ChangeCreate:
return NewCreateChange(
est,
oldChange.Env,
oldChange.BundleDefEntry,
schemactx,
inputDefs,
values,
)
case ChangeReconfig:
return NewReconfigChange(
est,
oldChange.OriginalBundle,
oldChange.BundleDefEntry,
schemactx,
inputDefs,
values,
)
case ChangePromote:
return NewPromoteChange(
est,
oldChange.Env,
oldChange.OriginalBundle,
oldChange.BundleDefEntry,
schemactx,
inputDefs,
values,
)
default:
panic("unsupported ChangeKind " + oldChange.Kind)
}
}

// reEvalAllInputs evaluates the bundle's input definitions using the prompted
// values as a simulated inputs block, filling in defaults for any inputs that
// were not prompted.
Expand Down Expand Up @@ -415,6 +384,47 @@ func reEvalAllInputs(
return result, nil
}

// normalizeBundleRefValues converts resolved bundle objects back to alias strings.
// When reconfiguring or promoting, bundle-ref inputs are loaded as full objects
// (with alias, uuid, etc.) from disk. The type system expects strings, so we
// extract the alias before the values enter the form.
func normalizeBundleRefValues(inputDefs []*config.InputDefinition, values map[string]cty.Value) map[string]cty.Value {
for _, def := range inputDefs {
if _, isBundleType := def.Type.(*typeschema.BundleType); !isBundleType {
continue
}
v, ok := values[def.Name]
if !ok || v == cty.NilVal || v.IsNull() || !v.IsKnown() {
continue
}
if v.Type().IsObjectType() && v.Type().HasAttribute("alias") {
alias := v.GetAttr("alias")
if alias.IsKnown() && alias.Type() == cty.String {
values[def.Name] = alias
}
}
}
return values
}

// checkBundleRefsResolved verifies that all bundle-ref inputs resolved to non-null
// values. Returns a user-friendly error if any referenced bundle is missing.
func checkBundleRefsResolved(inputDefs []*config.InputDefinition, values map[string]cty.Value) error {
for _, def := range inputDefs {
if _, isBundleType := def.Type.(*typeschema.BundleType); !isBundleType {
continue
}
v, ok := values[def.Name]
if !ok || v == cty.NilVal {
continue
}
if v.IsNull() {
return errors.E("Input %q references a bundle that does not exist in this environment. Promote dependencies first.", def.Name)
}
}
return nil
}

// Save writes the change to disk as a YAML bundle instance file.
func (c *Change) Save(envs []*config.Environment) error {
var existing *yaml.BundleInstance
Expand Down Expand Up @@ -449,6 +459,14 @@ func (c *Change) generateBundleYAML(existing *yaml.BundleInstance, envs []*confi
continue
}

// Bundle-ref values are stored as resolved objects internally
// but must be written as alias strings in the YAML config.
if _, isBundleType := def.Type.(*typeschema.BundleType); isBundleType {
if v.IsKnown() && !v.IsNull() && v.Type().IsObjectType() && v.Type().HasAttribute("alias") {
v = v.GetAttr("alias")
}
}

yv, err := yaml.ConvertFromCty(v)
if err != nil {
return "", err
Expand Down
Loading
Loading