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
- Variables
- func NewGrowStage(ctx initflow.StageContext, action initflow.GenerateAction) *initflow.GenerateStage
- type AgentOption
- type BranchesContext
- type BranchesResult
- type BranchesStage
- type ConflictsStage
- type GroundContext
- type GroundStage
- type ObserveStage
- func (s *ObserveStage) Init() tea.Cmd
- func (s *ObserveStage) Reset() tea.Cmd
- func (s *ObserveStage) Result() any
- func (s *ObserveStage) SetDefaultWorkspace(ws string)
- func (s *ObserveStage) SetPrior(prev []any)
- func (s *ObserveStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)
- func (s *ObserveStage) View() string
- type Outcome
- type SelectStage
- type YieldStage
- func NewYieldAllInstalled(ctx initflow.StageContext, agentDef *catalog.AgentDef) *YieldStage
- func NewYieldSuccess(ctx initflow.StageContext, installed *config.InstalledAgent, ...) *YieldStage
- func NewYieldTechLeadRequired(ctx initflow.StageContext, agentType string) *YieldStage
- func NewYieldUnknownAgent(ctx initflow.StageContext, agentType string) *YieldStage
Constants ¶
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 ¶
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 ¶
func NewGrowStage(ctx initflow.StageContext, action initflow.GenerateAction) *initflow.GenerateStage
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
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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) 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) 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.