Skip to content

feat(tui): design anatomy-based plugin attachment API #40749

Open

Description

@kitlangton

Problem

TUI plugin slots currently enumerate fixed positions such as prompt.footer.end. This does not scale when a plugin needs to render relative to existing UI elements. Adding increasingly specific positional slots would keep expanding the API without describing the UI structure plugins attach to.

Proposed API

Publish a small anatomy tree of stable, named host parts and let plugins attach relative to those parts.

context.ui.attach("prompt.footer", {
  after: "location",
  render: () => <text>content</text>,
})

The same operation can support container takeover:

context.ui.attach("prompt.footer", {
  replace: "right",
  render: () => <text>custom footer</text>,
})

Placement is a correlated sum:

type Placement =
  | { at: "start" | "end" }
  | { before: Part }
  | { after: Part }
  | { replace: Part }

The anatomy should describe the footer that exists today, including its host-owned left side:

prompt.footer
├── left
│   ├── location
│   └── status
└── right
    ├── editor-context
    └── contributions

location and status occupy the same left-side region conditionally: the normal state shows the current location, while active operations show status/progress there.

Proposed host behavior

  • Replacing a part suppresses attachments within that subtree.
  • Suppression is visible in the plugins UI rather than happening silently.
  • Ancestor replacement wins over descendant claims.
  • At the same node, the most recently enabled plugin wins.
  • User-hidden host parts retain their anchors so attached plugins can still render.
  • Under width pressure, guest plugin content is hidden before host content.
  • Plugins do not receive width queries; the host owns measurement and pressure policy.

Questions to grill

  • What naming and compatibility guarantees do anatomy part IDs need?
  • Should replace ship in the first version or follow relative attachment later?
  • How should separators and gaps behave when host parts are hidden or replaced?
  • Is most-recently-enabled ordering sufficiently predictable for conflict resolution?
  • What diagnostics should plugins receive when an attachment is suppressed or references a missing part?
  • Does the anatomy tree expose enough structure without coupling plugins too tightly to host layout?

Activity

  1. Dante-dan commented on Oct 3, 2026

    @Dante-dan
    Contributor

    I would make the first slice relative insertion within prompt.footer, and defer replace. The useful increment is placement next to a named host part; subtree takeover introduces a separate ownership/conflict model.

    The current public slot types expose fixed names such as home_prompt_right and session_prompt_right, while the host adapter delegates rendering to the OpenTUI registry. I would keep those APIs working and add anatomy placement at the footer host boundary, rather than change the meaning of existing slot modes.

    My proposed answers to the six questions:

    • IDs/compatibility: publish an explicit allowlist of semantic part IDs, qualified by their container. Do not derive IDs from JSX structure or expose arbitrary descendant paths. Existing IDs should keep their logical ordering when host markup changes; removals need an explicit migration rather than silently redirecting to end.
    • First version: support the correlated at / before / after union only. Reject unknown container/part pairs, including anchors belonging to a different container. Defer replacement, ancestor precedence and winner selection together; insertion does not need a winner.
    • Gaps: retain logical anchors when host content is hidden, but give an empty anchor no width or separator. The host inserts gaps between rendered items only. In particular, the location ↔ status transition must preserve attachment order: otherwise a plugin after location could jump merely because an operation starts.
    • Ordering: keep a fixed host-anchor order. For multiple guests at the same insertion point, use deterministic plugin order plus that plugin's registration order; toggling a plugin off/on should not rearrange its peers. “Most recently enabled wins” can be discussed later for replacement, where a winner actually exists.
    • Diagnostics: associate each attachment with its owning plugin and report missing_part separately from temporary hidden_for_width. Hidden host content is neither of those failures. Dispose attachments with the plugin scope, so disabling a plugin cannot leave ghost entries. Replacement-specific suppression diagnostics can follow replacement.
    • Coupling: commit to semantic anchors and relative order, without promising boxes, pixel/column positions, or width queries. The host keeps measurement and pressure policy.

    A bounded acceptance matrix would cover two plugins targeting the same anchor, the location/status transition, hidden host content without stray gaps, disable/re-enable and disposal, missing/wrong-container anchors, narrow-width guest suppression, and unchanged legacy slot behavior. These are proposed checks, not results from an implementation.

    Could the core team approve this insertion-only footer scope, or identify which semantics need to change first?

  2. moritzWa commented on Oct 4, 2026

    @moritzWa

    Two plugins I use daily would both be served by the insertion-only slice @Dante-dan describes, so I'd also ship that first and defer replace.

    • opencode-read-aloud shows playback controls (pause - 1.7x + stop) while an answer is being read.
    • A PR badge shows #123, coloured by PR state, and opens the PR on click.

    Both want to sit on the footer's left side next to location. Today they run on a local build that adds one more fixed slot (session_prompt_footer), which is exactly the slot growth this issue wants to stop. On stock, read-aloud falls back to session_prompt_right, where it competes with the agent/model row. With the proposed API, each becomes attach("prompt.footer", { after: "location", ... }).

    On width pressure: without a host policy, every plugin guesses. read-aloud collapses its controls below 80 terminal columns. opencode-session-title measures its own box with onSizeChange and hides below a minimum width. Neither knows what else shares the row. "Guest content hides before host content" would replace both hacks.

    If the insertion-only footer scope is approved, I'm happy to build it.

  3. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up. This is a design proposal for a new plugin API rather than a bug, so there was nothing to reproduce. The current fixed-slot API (prompt.footer, prompt.footer.status, prompt.footer.file, rendered through nested <Slot>s in the prompt component) works as designed today. I'm leaving this for the team to work through the open design questions in the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    gang-grillDesign topics queued for later group grilling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions