removeflow

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: May 7, 2026 License: MIT Imports: 8 Imported by: 0

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

View Source
const (
	ModeAgentRemove removeMode = iota
	ModeItemRemove
)
View Source
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

View Source
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

type AbilityCounts struct {
	Skills    int
	Workflows int
	Protocols int
	Sensors   int
	Routines  int
}

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

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

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) Update

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

Update handles focus toggle + y/n shortcuts + Enter commit.

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

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

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 ObserveStage

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

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) Update

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

Update handles Enter (advance) and Esc (back).

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

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

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) Update

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

Update handles focus movement + Enter-to-advance.

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). 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) Init

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

Init implements tea.Model.

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) 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 the chromeless exit frame.

Jump to

Keyboard shortcuts

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