Help us shape the future of Go! Take the 2026 Go Developer Survey

addflow

package
v0.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package addflow implements the cinematic 4-stage `bonsai add` flow.

The package mirrors internal/tui/initflow's shape — chromeless stages that compose a shared persistent chrome (header + enso rail + footer) around a per-stage body — but with a 4-segment rail specific to the add journey:

選 SELECT  枝 BRANCHES  観 OBSERVE  結 YIELD

Every chrome primitive (RenderHeader, RenderEnsoRail, RenderFooter, RenderMinSizeFloor, ClampColumns, Viewport, PanelContentWidth, design tokens) is imported from initflow — addflow does not reimplement any of them. The rail length adapts because initflow.RenderEnsoRail accepts an explicit label slice (Plan 23 Phase 1 refactor).

Plan 23 reference: station/Playbook/Plans/Active/23-uiux-phase2-add.md. Plan 27 reference: station/Playbook/Plans/Active/27-add-flow-polish.md — shrinks the visible rail from 7 → 4 segments; Ground, Grow, and Conflicts stages remain in the splicer but render off-rail (rail index sentinel StageIdxOffRail).

Index

Constants

View Source
const (
	StageIdxSelect   = 0
	StageIdxBranches = 1
	StageIdxObserve  = 2
	StageIdxYield    = 3

	// StageIdxOffRail is the sentinel rail index for stages that render
	// without a visible rail tab (Ground, Grow, Conflicts). The base Stage
	// skips the rail row when its idx is negative.
	StageIdxOffRail = -1
)

Stage indices in the add-flow rail. Kept as named constants so ctors and splicers reference them by name rather than literal integers. Plan 27 shrinks the rail to four visible stages; Ground, Grow, and Conflicts stages are off-rail and use StageIdxOffRail as their rail index sentinel.

Variables

View Source
var StageLabels = []initflow.StageLabel{
	{Kanji: "選", Kana: "えらぶ", English: "SELECT"},
	{Kanji: "枝", Kana: "えだ", English: "BRANCHES"},
	{Kanji: "観", Kana: "みる", English: "OBSERVE"},
	{Kanji: "結", Kana: "むすぶ", English: "YIELD"},
}

StageLabels holds the four canonical add-flow stage labels in order. Matches Plan 27's rail canon — Ground / Grow / Conflicts stages still exist as steps in the splicer but do not appear as rail tabs; their rail index is StageIdxOffRail and the base Stage.renderFrame skips the rail render when the index is negative.

選 えらぶ    Select    — pick the agent
枝 えだ      Branches  — abilities (skills/workflows/protocols/sensors/routines)
観 みる      Observe   — one last look before the write
結 むすぶ    Yield     — completion card

Functions

func NewGrowStage

NewGrowStage constructs the Grow stage (育 GROW). Renders off-rail — the visible rail stays anchored on OBSERVE while Grow runs so there is no rail churn between Observe and the spinner.

Thin wrapper around initflow.NewGenerateStage — installs the add-flow rail labels on the embedded Stage and overrides the body title kanji from 生 (init's Generate) to 育 (Grow) so the visual language matches the bonsai-raising metaphor of the add flow.

action is the same GenerateAction contract as the init flow — a one-shot func() error that runs the write pipeline and returns nil on success. The underlying min-hold + animation behaviour is unchanged.

Types

type AgentOption

type AgentOption struct {
	Name        string // machine identifier returned verbatim in Result
	DisplayName string // human-readable label shown in the row
	Description string // one-line caption rendered muted after the name
	Installed   bool   // true when cfg.Agents[name] exists
}

AgentOption is the per-row shape consumed by SelectStage. Deliberately minimal — the stage needs a machine name, a display label, a description, and a flag indicating whether the agent type is already installed (renders as an "(installed)" suffix).

func BuildAgentOptions

func BuildAgentOptions(cat *catalog.Catalog, installed map[string]bool) []AgentOption

BuildAgentOptions maps catalog agents into SelectStage's row shape, flagging each entry as Installed when the config already contains that agent type. Catalog order is preserved verbatim.

type BranchesContext added in v0.3.0

type BranchesContext struct {
	Cat       *catalog.Catalog
	AgentType string
	AgentDef  *catalog.AgentDef
	// Installed is nil on the new-agent branch; populated on add-items.
	Installed *config.InstalledAgent
}

BranchesContext bundles everything a Branches ctor needs. Kept separate from StageContext so the add-items branch can pass an Installed pointer without polluting the shared context type.

type BranchesResult added in v0.3.0

type BranchesResult struct {
	Skills    []string
	Workflows []string
	Protocols []string
	Sensors   []string
	Routines  []string
}

BranchesResult is the advance-payload returned from BranchesStage.Result() on Enter. Slices preserve catalog iteration order (alphabetical per catalog.loadItems). Required items are always present. Mirrors initflow.BranchesResult shape so the action closure can read either path with a type switch.

func (BranchesResult) Total added in v0.3.0

func (r BranchesResult) Total() int

Total returns the sum of selection counts across the five categories. Used by Observe for the CTA's "Graft ~N items" line.

type BranchesStage added in v0.3.0

type BranchesStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

BranchesStage is the tabbed ability picker at rail position 1 (枝 BRANCHES). Keystroke model matches initflow.BranchesStage: ← → tab, ↑ ↓ focus, ␣ toggle, ? details, ↵ advance. Required items are pre-selected and immutable; defaults are pre-selected but toggleable.

Result: BranchesResult with per-category slices of selected machine names.

func NewAddItemsBranches added in v0.3.0

func NewAddItemsBranches(ctx initflow.StageContext, gctx BranchesContext) *BranchesStage

NewAddItemsBranches constructs a Branches stage for the add-items branch — filters each category to uninstalled items, drops empty tabs.

func NewNewAgentBranches added in v0.3.0

func NewNewAgentBranches(ctx initflow.StageContext, gctx BranchesContext) *BranchesStage

NewNewAgentBranches constructs a Branches stage for the new-agent branch — all five tabs, defaults seeded from agentDef.

func (*BranchesStage) Init added in v0.3.0

func (s *BranchesStage) Init() tea.Cmd

Init implements tea.Model — nothing to fire on entry.

func (*BranchesStage) Reset added in v0.3.0

func (s *BranchesStage) Reset() tea.Cmd

Reset clears the completion flag. Per-tab selections, cursor positions, and the inline-expand toggle are all preserved across Esc-back.

func (*BranchesStage) Result added in v0.3.0

func (s *BranchesStage) Result() any

Result returns a BranchesResult with per-category slices of selected machine names, preserving catalog iteration order. Required items are always present.

func (*BranchesStage) Update added in v0.3.0

func (s *BranchesStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update handles tab cycling, row focus, toggle, inline-expand, and Enter.

func (*BranchesStage) View added in v0.3.0

func (s *BranchesStage) View() string

View composes the stage body inside the shared frame.

type ConflictsStage

type ConflictsStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

ConflictsStage is the chromeless full-screen conflict-resolution surface (Plan 27 PR2 §C1-C5). Rendered only when the Grow action produced at least one ActionConflict file — the harness gates construction on wr.HasConflicts(), so this stage never instantiates for clean writes.

Layout: one row per conflict file in a vertical list. Each row carries:

[focus glyph] [action glyph · coloured by pick] [relative path] [action label]

Below the list a batch-resolve row renders three cells (Keep all / Overwrite all / Backup all) that apply the chosen action to every row via uppercase K/O/B.

The stage is chromeless — View() returns the full AltScreen frame without the header/enso-rail/footer chrome used by the four on-rail stages. The rail visible to the user (anchored on OBSERVE) stays unchanged while the conflict picker runs — no rail churn between Observe and Conflict.

Keystrokes (plan-27 PR2 §C2 + §C4):

  • ↑ ↓ / j k move focus row (no wrap)
  • 1 / 2 / 3 set focused row's action to Keep / Overwrite / Backup
  • ␣ cycle focused row's action (Keep → Overwrite → Backup → Keep)
  • K / O / B batch-resolve — apply action to every row
  • ↵ advance (complete stage)
  • shift+tab / esc back to Observe (harness pops cursor)

Result: map[string]config.ConflictAction keyed by FileResult.RelPath — one entry per conflict file. applyCinematicConflictPicks in cmd/add.go reads the map and dispatches per-file mutations.

func NewConflictsStage

func NewConflictsStage(ctx initflow.StageContext, wr *generate.WriteResult) *ConflictsStage

NewConflictsStage constructs the stage over the conflict entries present in wr. When wr has zero conflicts the ctor still returns a usable stage — it simply renders an empty body with a single "nothing to reconcile" line and completes on Enter. Callers should gate on wr.HasConflicts() before splicing.

func (*ConflictsStage) Chromeless added in v0.3.0

func (s *ConflictsStage) Chromeless() bool

Chromeless reports true so the harness yields View() verbatim without its default header/footer. Embedded initflow.Stage.Chromeless() already returns true, but ConflictsStage declares the method explicitly so the contract is obvious at call-sites that type-scan for Chromeless.

func (*ConflictsStage) Init

func (s *ConflictsStage) Init() tea.Cmd

Init implements tea.Model — no cmd on entry.

func (*ConflictsStage) Reset

func (s *ConflictsStage) Reset() tea.Cmd

Reset clears the completion flag but preserves per-file picks and focus so Esc-back → re-entry reads exactly where the user left off.

func (*ConflictsStage) Result

func (s *ConflictsStage) Result() any

Result returns the per-file ConflictAction map. Keys are RelPath values; every conflict entry is guaranteed to appear in the map (default Keep if untouched).

func (*ConflictsStage) Update

func (s *ConflictsStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update handles focus movement + per-row action + batch-resolve + advance + back.

func (*ConflictsStage) View

func (s *ConflictsStage) View() string

View returns the full AltScreen frame. Chromeless — no header/rail/footer. Mirrors initflow.PlantedStage.View for layout parity: centre vertically, compose title + divider + list + batch row + inline key hints.

type GroundContext

type GroundContext struct {
	AgentType          string
	DocsPath           string          // cfg.DocsPath for tech-lead fallback
	ExistingWorkspaces map[string]bool // keys already taken by installed agents
}

GroundContext is the ctor bundle for GroundStage. Mirrors the shape used by the other addflow ctors so cmd/add.go can stamp everything in one place.

type GroundStage

type GroundStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

GroundStage collects the workspace directory for a new non-tech-lead agent at rail position 1 (地 GROUND). Single textinput with Vessel-style focus underline + unique-workspace validator. For the tech-lead agent type the stage auto-completes with the resolved DocsPath (or "station/") without any keystroke — AutoComplete() drives the harness's skip-past.

Result: resolved workspace string (always trailing-slashed + cleaned).

func NewGroundStage

func NewGroundStage(ctx initflow.StageContext, gc GroundContext) *GroundStage

NewGroundStage constructs the Ground stage. When agentType is "tech-lead" the stage is in auto-complete mode — AutoComplete() returns (workspace, true) and the harness's NewLazy wrapper should skip it.

func (*GroundStage) AutoComplete

func (s *GroundStage) AutoComplete() bool

AutoComplete reports true when there is nothing for the user to change on this stage (tech-lead path) so the harness's Esc-back walker skips past it. Matches the SpinnerStep / MultiSelectStep(all required) convention.

func (*GroundStage) Chromeless added in v0.3.0

func (s *GroundStage) Chromeless() bool

Chromeless reports true so the harness yields View() verbatim without its default header/footer wrap. Plan 27 PR2 §C6 — GroundStage renders a centred off-rail form panel without the enso rail / header / footer chrome used by the four on-rail stages. The rail visible to the user stays anchored on SELECT while Ground collects the workspace.

func (*GroundStage) Init

func (s *GroundStage) Init() tea.Cmd

Init kicks the textinput cursor blink. For the tech-lead agent the stage is non-interactive — flip Done immediately so the harness forwards past the stage without a keystroke.

func (*GroundStage) Reset

func (s *GroundStage) Reset() tea.Cmd

Reset clears completion so re-entry doesn't auto-advance. Preserves the entered value.

func (*GroundStage) Result

func (s *GroundStage) Result() any

Result returns the resolved workspace string (normalised trailing slash) or the tech-lead default when in auto-complete mode.

func (*GroundStage) Update

func (s *GroundStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update handles typing + Enter-to-validate-and-advance. Esc propagates to the harness (pops back to Select).

func (*GroundStage) View

func (s *GroundStage) View() string

View returns the full AltScreen frame for the Ground stage. Chromeless — body centred vertically, inline key-hint row below the form. Mirrors the layout rhythm of initflow.PlantedStage (centre body + inline hint) so the off-rail Ground reads as part of the same cinematic.

type ObserveStage

type ObserveStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

ObserveStage is the pre-write "one last look" stage at rail position 2 (観 OBSERVE). Composes a read-only summary of the Select / Ground / Branches results into a single review panel with a GRAFT / BACK CTA.

Result:

  • true → user confirmed GRAFT — harness proceeds to Grow.
  • false → user cancelled — flow exits without writes.

Prior inputs arrive via SetPrior so the stage can render fresh summaries even after an Esc-back edit pass upstream.

func NewObserveStage

func NewObserveStage(ctx initflow.StageContext, cat *catalog.Catalog) *ObserveStage

NewObserveStage constructs the Observe stage.

func (*ObserveStage) Init

func (s *ObserveStage) Init() tea.Cmd

Init implements tea.Model — no cmd on entry.

func (*ObserveStage) Reset

func (s *ObserveStage) Reset() tea.Cmd

Reset clears completion + confirmation while preserving the captured prior snapshot and button focus.

func (*ObserveStage) Result

func (s *ObserveStage) Result() any

Result returns a bool: true = proceed to Grow, false = cancel.

func (*ObserveStage) SetDefaultWorkspace

func (s *ObserveStage) SetDefaultWorkspace(ws string)

SetDefaultWorkspace seeds a fallback workspace value rendered when the prior-results snapshot does not carry one. Used on the add-items branch, where Ground is skipped — the installed agent's existing workspace is stamped into the stage before SetPrior runs so the Observe panel reads the durable workspace rather than "—".

func (*ObserveStage) SetPrior

func (s *ObserveStage) SetPrior(prev []any)

SetPrior captures Select / Ground / Graft results. Called on entry and after every Esc-back edit.

The add-flow splices differ in shape across branches — new-agent writes (agent, workspace, graft, ...) while add-items writes (agent, graft, ...) because Ground is skipped. Rather than hard-code positional indices (which break the add-items branch), SetPrior scans prev[] for the first value of each expected type. BranchesResult is uniquely typed, so the match is unambiguous; agent / workspace are both strings but agent is always prev[0] (from SelectStage) so we grab it first and treat any later string as the workspace.

func (*ObserveStage) Update

func (s *ObserveStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update handles button toggle + Enter confirm + y/n shortcuts.

func (*ObserveStage) View

func (s *ObserveStage) View() string

View composes the body inside the shared frame.

type Outcome

type Outcome struct {
	AgentDef      *catalog.AgentDef
	Workspace     string
	NewAgent      bool
	TotalSelected int
	SpinnerErr    error
	Ran           bool
}

Outcome is the cross-stage scratchpad populated by the Grow action and consumed by Yield + the post-harness cleanup in cmd/add.go. Kept in addflow (not cmd/) so test helpers can stamp synthetic outcomes without back-importing cmd.

Field semantics:

  • Ran — true once the Grow action body executed (even on error).
  • AgentDef — resolved catalog AgentDef (nil on unknown-agent error).
  • Workspace — normalised workspace path actually written.
  • NewAgent — true for the new-agent branch, false for add-items.
  • TotalSelected — BranchesResult.Total() captured at write time.
  • SpinnerErr — non-nil when the write pipeline failed; routed to a tui.Warning by the caller and short-circuits the Yield render.

type SelectStage

type SelectStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

SelectStage is the single-choice agent picker at rail position 0. Renders the full agent catalog as a Branches-style list — Leaf "│ " focus border, white-bold focused name, ColorRule2 description on unfocused rows — and appends an "(installed)" suffix after any agent already present in the config.

Result: machine name of the chosen agent (string).

func NewSelectStage

func NewSelectStage(ctx initflow.StageContext, options []AgentOption) *SelectStage

NewSelectStage constructs the Select stage. options is typically built via BuildAgentOptions against the loaded catalog + project config.

func (*SelectStage) Init

func (s *SelectStage) Init() tea.Cmd

Init implements tea.Model — no cmd to fire on entry.

func (*SelectStage) Reset

func (s *SelectStage) Reset() tea.Cmd

Reset clears the completion flag so re-entry renders fresh. Focus index + option slice are preserved so the user's pick is not scrubbed.

func (*SelectStage) Result

func (s *SelectStage) Result() any

Result returns the selected agent machine name. Zero value ("") on empty option slice or pre-completion.

func (*SelectStage) Update

func (s *SelectStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update handles focus movement + Enter-to-advance. Esc is consumed by the harness at the root — stage 0 esc is a no-op (aborts only via Ctrl-C).

func (*SelectStage) View

func (s *SelectStage) View() string

View composes the Select stage body inside the shared frame.

type YieldStage

type YieldStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

YieldStage is the terminal completion card at rail position 3 (StageIdxYield, 結 YIELD). Four variants selected at construction time:

  • success — renders the installed ability tree + 3 next-steps.
  • all-installed — "already full" panel + `bonsai catalog` CTA.
  • tech-lead-req — error panel + `bonsai init` CTA.
  • unknown-agent — error panel + `bonsai update` CTA (catalog stale).

The stage is terminal: ↵ / q / esc flip Done and the harness exits.

func NewYieldAllInstalled

func NewYieldAllInstalled(ctx initflow.StageContext, agentDef *catalog.AgentDef) *YieldStage

NewYieldAllInstalled renders the "every ability already installed" card.

func NewYieldSuccess

func NewYieldSuccess(ctx initflow.StageContext, installed *config.InstalledAgent, cat *catalog.Catalog, isNewAgent bool, totalSelected int) *YieldStage

NewYieldSuccess renders the happy-path completion card. installed is the InstalledAgent populated by the Grow action; cat is the loaded catalog; isNewAgent distinguishes new-agent vs add-items success messaging; totalSelected drives the add-items summary line.

func NewYieldTechLeadRequired

func NewYieldTechLeadRequired(ctx initflow.StageContext, agentType string) *YieldStage

NewYieldTechLeadRequired renders the "tech-lead must come first" card.

func NewYieldUnknownAgent

func NewYieldUnknownAgent(ctx initflow.StageContext, agentType string) *YieldStage

NewYieldUnknownAgent renders the "unknown agent type" card. Picked when the user asks for an agent name the loaded catalog does not recognise — usually a stale binary against a newer .bonsai.yaml. The CTA is `bonsai update` so the user has a concrete next step instead of a silent failure.

func (*YieldStage) Chromeless added in v0.3.0

func (s *YieldStage) Chromeless() bool

Chromeless reports true so the harness yields View() verbatim. Plan 27 PR2 §C9 — YieldStage renders its own chromeless full-screen frame (no enso rail, no header, no footer), matching initflow.PlantedStage's exit-card treatment. Embedded initflow.Stage.Chromeless() already reports true, but the explicit method here keeps the contract obvious at the call-site.

func (*YieldStage) Init

func (s *YieldStage) Init() tea.Cmd

Init implements tea.Model — no cmd on entry.

func (*YieldStage) Reset

func (s *YieldStage) Reset() tea.Cmd

Reset clears the completion flag.

func (*YieldStage) Result

func (s *YieldStage) Result() any

Result returns nil — Yield is the terminal stage.

func (*YieldStage) SetHintBlock added in v0.3.0

func (s *YieldStage) SetHintBlock(block string)

SetHintBlock installs the pre-rendered hints.Render output. Replaces the built-in NEXT callout on the success variant. Plan 31 Phase H.

func (*YieldStage) Update

func (s *YieldStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update waits for ↵ / q / esc acknowledgement.

func (*YieldStage) View

func (s *YieldStage) View() string

View renders Yield as a chromeless, vertically-centred exit card. Mirrors initflow.PlantedStage.View — no header/rail/footer chrome, body centred in the live AltScreen height, inline "↵ exit · q quit" hint. The terminal-too- small floor still gates via initflow.RenderMinSizeFloor.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL