refactor(mecatui): surface abstraction for modals/overlays (keys + wheel + render + lifecycle in one place)
@jbeda is already working on this.
Since Aug 15, 2026.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Problem
Adding a new modal/overlay to mecatui today touches seven places, all held in lockstep by convention:
- a phase or Model flag (e.g.
argsViewOpen), - a
renderBodyarm, - an
onKey/dispatchPhaseKeybranch (e.g.onApprovalKey's cascade), - an
onMouseWheelbranch, - an
askButtonAt/hit-test arm inclickgeom.go, - lifecycle clears (
advanceAsk,endRun,resetSession, retract), - the help overlay's key listing.
PR #553 (issue #488) drifted on two of these within one feature (the render path didn't thread the scroll offset; the idle-resume phase was wrong), which shows the convention is a weak gate. This came out of the code review discussion on #553.
Proposal
A surface interface so each modal/overlay owns its behavior in one file:
type surface interface {
Render() string // body content
Regions() []ClickableRegion // emitted by the layout pass, not re-derived
HandleKey(tea.KeyPressMsg) (Action, bool) // keys the surface consumes
HandleWheel(tea.MouseWheelMsg) (Action, bool)
Close() // lifecycle teardown
}
The Model holds a stack of active surfaces and routes render/keys/wheel/clicks to the top one — replacing the per-surface branches in renderBody, onKey, onMouseWheel, and the hit-test arms. Clickable regions carry a semantic Action payload (a sum type, not a func — value-safe across Model copies, testable), so the hit-test returns the action and ONE dispatcher executes it; no second mapping table.
Migration plan
- The click half (region-emitting builders +
ClickableRegion) lands in PR #553 as the vocabulary/pattern proof. - Migrate the approval surface first (it has the most arms: plan / args-view / generic modal), then the other overlays (MCP, team/agents, skills, soul, sessions, models, worktrees, schedule) one at a time.
- The seven-touchpoint list above becomes the acceptance checklist: a new surface should touch ONE file + ONE registration point.
Non-goals
- No widget hierarchy / retained-mode GUI framework — this stays Elm-architecture; surfaces are rebuilt from state every render (regions are emitted per layout pass, so they are always as fresh as the frame).
- No change to the wire/proto surface or the keymap registry semantics.
See also: PR #553 discussion (click geometry origin), ADR 0108.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.