Documentation
¶
Overview ¶
Package removeflow implements the cinematic 4-stage `bonsai remove` flow.
The package mirrors internal/tui/addflow's shape — chromeless stages that compose the shared persistent chrome (header + enso rail + footer) from initflow around a per-stage body — but with a 4-segment rail tuned for the remove journey:
択 SELECT 観 OBSERVE 確 CONFIRM 結 YIELD
The Conflicts stage is spliced in chromelessly (rail index StageIdxOffRail) when the post-generate write-pipeline produces user-modified files, so the visible rail does not churn between Confirm and Yield.
Two entry shapes:
- Agent removal (`bonsai remove <agent>`) — Select is skipped; Observe previews the installed agent's ability tree, Confirm gates the write, Conflicts reconciles per-file picks, Yield shows the summary card.
- Item removal (`bonsai remove skill foo`) — Select fires iff multiple agents have the item installed; Observe, Confirm, Conflicts, Yield share the same shape as agent-remove.
Plan 31 reference: station/Playbook/Plans/Active/31-v03-release-readiness.md. Phase E — port `bonsai remove` from the raw harness + tui.FatalPanel path to a dedicated flow package, matching the Plan 22/23/27/28/29/30 cinematic rollout across init/add/list/catalog/guide.
Index ¶
Constants ¶
const ( ModeAgentRemove removeMode = iota ModeItemRemove )
const ( StageIdxSelect = 0 StageIdxObserve = 1 StageIdxConfirm = 2 StageIdxYield = 3 // StageIdxOffRail is the sentinel rail index for Conflicts — the base // Stage skips the rail row when its idx is negative. StageIdxOffRail = -1 )
Stage indices in the remove-flow rail. Conflicts renders off-rail so the visible 4-segment rail stays stable when the conflicts picker splices in.
Variables ¶
var StageLabels = []initflow.StageLabel{
{Kanji: "択", Kana: "えらぶ", English: "SELECT"},
{Kanji: "観", Kana: "みる", English: "OBSERVE"},
{Kanji: "確", Kana: "かくにん", English: "CONFIRM"},
{Kanji: "結", Kana: "むすぶ", English: "YIELD"},
}
StageLabels holds the four canonical remove-flow stage labels in order. Ordering matches the flow's state machine: pick the target (when ambiguous), preview what will be removed, confirm the destructive action, report.
択 えらぶ Select — pick the agent (iff item-remove with multiple matches) 観 みる Observe — preview what will be removed 確 かくにん Confirm — explicit yes/no gate before the write 結 むすぶ Yield — completion card
Functions ¶
This section is empty.
Types ¶
type AbilityCounts ¶
AbilityCounts captures how many installed abilities each category carries for the agent being removed (agent-remove branch) or the target(s) of an item removal. Used by Observe's preview panel and Yield's summary.
func (AbilityCounts) Total ¶
func (c AbilityCounts) Total() int
Total returns the sum across categories.
type AgentOption ¶
type AgentOption struct {
Name string // machine identifier, or "_all_" for the aggregate row
DisplayName string // human-readable label shown in the row
Workspace string // workspace path, rendered muted after the name
All bool // true for the aggregate "All agents" row
}
AgentOption is the per-row shape consumed by SelectStage on the item-remove branch. Rows are one agent that has the named ability installed; selection returns the machine name (or "_all_" for the aggregate row).
type ConfirmStage ¶
ConfirmStage is the chromeless full-screen destructive-gate at rail position 2 (確 CONFIRM). Renders a centered prompt + UPROOT / BACK buttons. Default focus is BACK — the destructive action is opt-in, matching the addflow.ObserveStage pattern but with inverted defaults (remove is destructive).
Keystrokes:
- tab / ← → / h / l toggle focus
- y / Y confirm uproot
- n / N cancel
- ↵ commit focused button
- esc back (harness pops cursor)
Result: bool. true → proceed to action + Yield; false → abort.
func NewConfirmStage ¶
func NewConfirmStage(ctx initflow.StageContext, heading, detail, caption string) *ConfirmStage
NewConfirmStage constructs a chromeless confirm gate.
func (*ConfirmStage) Chromeless ¶
func (s *ConfirmStage) Chromeless() bool
Chromeless reports true so the harness yields View() verbatim without its default header/footer. Matches addflow.ConflictsStage pattern.
func (*ConfirmStage) Init ¶
func (s *ConfirmStage) Init() tea.Cmd
Init implements tea.Model — no cmd on entry.
func (*ConfirmStage) Reset ¶
func (s *ConfirmStage) Reset() tea.Cmd
Reset clears completion + confirmation but preserves focus so re-entry after an esc-back lands where the user left off.
func (*ConfirmStage) Result ¶
func (s *ConfirmStage) Result() any
Result returns the user's pick as a bool.
func (*ConfirmStage) View ¶
func (s *ConfirmStage) View() string
View renders the chromeless full-screen frame. Mirrors ConflictsStage and YieldStage layouts: body centered vertically in the AltScreen.
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 ¶
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 ObserveStage ¶
ObserveStage is the pre-confirm preview at rail position 1 (観 OBSERVE). Agent-remove: shows the full ability tree installed on the target agent. Item-remove: shows the item name + type + targets.
Unlike addflow's Observe, this stage is display-only — it does not gate on user input. The Confirm stage handles the destructive yes/no decision.
Result: nil. The harness advances on Enter; SelectStage's result (if any) stays in the prev[] slice for Confirm to consume.
func NewObserveAgent ¶
func NewObserveAgent(ctx initflow.StageContext, agentName, agentDisplay, workspace string, skills, workflows, protocols, sensors, routines []string) *ObserveStage
NewObserveAgent constructs Observe in agent-remove mode.
func NewObserveItem ¶
func NewObserveItem(ctx initflow.StageContext, itemDisplay, itemType string, targets []AgentOption) *ObserveStage
NewObserveItem constructs Observe in item-remove mode. targets may be overwritten on entry via SetTargets when the upstream Select stage resolves the picker choice into a concrete target list.
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 on re-entry.
func (*ObserveStage) Result ¶
func (s *ObserveStage) Result() any
Result returns nil — Observe is a preview, not a decision point.
func (*ObserveStage) SetTargets ¶
func (s *ObserveStage) SetTargets(targets []AgentOption)
SetTargets overwrites the item-remove target list — used when the upstream Select stage resolves the picker into a concrete subset.
func (*ObserveStage) View ¶
func (s *ObserveStage) View() string
View composes the Observe body inside the shared frame.
type Outcome ¶
type Outcome struct {
// Ran is true once the action closure executed (even on error).
Ran bool
// Err is non-nil when the write pipeline failed; routed to a warning by
// the caller and short-circuits the Yield success render.
Err error
}
Outcome is the cross-stage scratchpad populated by the action closure and consumed by the post-harness cleanup in cmd/remove.go. Kept here (not in cmd/) so test helpers can stamp synthetic outcomes without back-importing cmd.
type SelectStage ¶
SelectStage is the item-remove agent picker at rail position 0. Rendered only when the caller has computed that more than one installed agent carries the target ability — single-match removals skip this stage.
Result: machine name of the chosen agent ("_all_" for the aggregate row).
func NewSelectStage ¶
func NewSelectStage(ctx initflow.StageContext, itemDisplay, itemType string, options []AgentOption) *SelectStage
NewSelectStage constructs the remove-flow agent picker. options must carry at least two rows (the caller gates on len(matches) > 1 before instantiating) and typically ends with an aggregate "All agents" entry so the user can remove from every installed target in one pass.
func (*SelectStage) Init ¶
func (s *SelectStage) Init() tea.Cmd
Init implements tea.Model — no cmd on entry.
func (*SelectStage) Reset ¶
func (s *SelectStage) Reset() tea.Cmd
Reset clears the completion flag so re-entry renders fresh.
func (*SelectStage) Result ¶
func (s *SelectStage) Result() any
Result returns the selected agent's machine name, or "" when the stage has no options (defensive — ctor is gated on len > 1).
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). Renders a chromeless, vertically-centered exit card matching addflow.YieldStage's layout beat.
Two modes:
- agent-success — "N abilities uprooted · <agent> · lock synced"
- item-success — "<item> removed from <target>"
Hints block — Phase E ships a minimal 2-layer placeholder (next CLI + workflow tip). The 3-layer contract (catalog-driven next_cli / next_workflow / ai_prompts) integrates during the Plan 31 PR2 merge when β's Phase H hints infrastructure lands; this stage renders a stable surface either way.
func NewYieldAgentSuccess ¶
func NewYieldAgentSuccess(ctx initflow.StageContext, agentDisplay, workspace string, counts AbilityCounts) *YieldStage
NewYieldAgentSuccess renders the agent-remove happy-path card.
func NewYieldItemSuccess ¶
func NewYieldItemSuccess(ctx initflow.StageContext, itemDisplay, itemType string, targets []AgentOption) *YieldStage
NewYieldItemSuccess renders the item-remove happy-path card.
func (*YieldStage) Chromeless ¶
func (s *YieldStage) Chromeless() bool
Chromeless matches addflow.YieldStage — the stage renders a full-screen chromeless exit card (no header/rail/footer chrome).
func (*YieldStage) Reset ¶
func (s *YieldStage) Reset() tea.Cmd
Reset clears completion on re-entry.
func (*YieldStage) Result ¶
func (s *YieldStage) Result() any
Result returns nil — Yield is terminal.
func (*YieldStage) View ¶
func (s *YieldStage) View() string
View renders the chromeless exit frame.