agentic-ui-message-writer

star 514

MANDATORY before editing MessageWriter, PermissionManager, tool-call block rendering, sender headers, thinking blocks, auto-scroll, folds, status rows, permission buttons, or chat-buffer tool-call tests.

carlos-algms By carlos-algms schedule Updated 6/7/2026

name: agentic-ui-message-writer description: > MANDATORY before editing MessageWriter, PermissionManager, tool-call block rendering, sender headers, thinking blocks, auto-scroll, folds, status rows, permission buttons, or chat-buffer tool-call tests.

Agentic UI Message Writer

This skill covers chat-buffer content state. For widget windows, layout, fallback windows, hidden floats, and buffer redirection, read lua/agentic/ui/AGENTS.md first.

Hard rules

  • wrap stays on.
  • Cursor positioning is G0zb, not G$zb.
  • Cursor sits on the trailing "" line below the last block, never inside a tool-call block.
  • scrolloff = 4 on chat keeps room for spinner virt_lines above the cursor.
  • Auto-scroll captures before mutation and applies after mutation in the same tick. No vim.schedule between them.
  • Tool-call body updates replace only the body between stable anchor pads.
  • Manual folds only. Never foldexpr; read ADR 0001 before proposing foldexpr workarounds.
  • tracker._rendered_button_count is render-path-only state.

Tool-call layout

row 0          header         rewritten on every update, NOT folded
row 1          "" top_pad     fold start anchor
row 2..M-1     body           replaced on every update
row M          "" bottom_pad  fold end anchor
row M+1..M+K   permission     K rows: N button rows + N empty spacer rows
row M+K+1      status row     real text, outside the fold
  • K = 2 * N for N permission options.
  • Permission rows are outside the fold.
  • MessageWriter:_render_permission_section owns all permission rows plus the status row.
  • Pads are unconditional.
  • Header is rewritten because providers send placeholder titles before final titles.

Permission rows

  • Buttons live one-per-row between bottom_pad and the status row.
  • Empty spacer rows sit between buttons, with one trailing spacer above the status row.
  • Digit keymaps are bound only while a block is focused.
  • Cycle keys and <CR> are row-gated to button rows or the focused block's status row.
  • Spacer rows fall through to default motion.
  • Focus transition repaints old and new status rows.

Regression anchors:

  • permission_manager.test.lua::digit keymap lifecycle::"rebinds digit keymaps with new mapping after focus transition"
  • permission_manager.test.lua::bracket cycle::"focus transition triggers exactly 2 status-row repaints"

Special write paths

Use only the normal write path outside these cases.

Method When to use
write_structural_message Welcome banner on session open; banner before restore
write_restoring_message Per-message replay during session restore
replay_history_messages Provider switch only; bulk repaint from in-memory history
  • Outside restore/provider-switch, use write_message_chunk or write_tool_call_block.
  • replay_history_messages does not re-issue ACP send_prompt.
  • Adding a bulk-write path requires a new row here and a test.

Sender classification

MessageWriter:_maybe_write_sender_header maps update.sessionUpdate to sender. New ACP update types must be classified here or they get no header.

user_message_chunk     -> user
agent_message_chunk    -> agent
agent_thought_chunk    -> agent
tool_call              -> agent
plan                   -> no header

Thinking blocks reuse one extmark in NS_THINKING. Any non-thought write must clear thinking state first or the next thought extends the wrong extmark.

TodoList

  • TodoList owns ChatWidget.buf_nrs.todos.
  • It opens after diagnostics in WidgetLayout.
  • It is gated by Config.windows.todos.display.
  • It stays hidden until the first Plan event.
  • It auto-closes when all tasks complete.
  • It has no keymaps.

Traps

  • vim.schedule between mutation and G0zb: redraw can run with stale topline.
  • Replacing the whole tool-call range: manual fold dies.
  • Re-rendering tool-call body after tracker.diff exists: preview consistency breaks. Once a diff exists, refresh only header/status.
  • Overwriting status/button rows while permission is pending: buttons disappear until the next focus repaint. Use _render_permission_section.

Test invariants

Each invariant has an existing regression test. Deleting one is a behavior change.

  • Fold survives close and reopen.
  • Fold creation is gated by screen-row count.
  • Fold counts wrapped rows, not buffer lines.
  • Status row and permission rows are real text rendered per state.
  • Block range extmark grows by K on permission render.
  • Focus transition triggers exactly two status-row repaints.
  • Digit keymap dispatches the focused block option.
  • Bracket cycle wraps and no-ops when pending is empty.
  • Concurrent permission map preserves insertion order and supports out-of-order resolve.
  • Sender headers deduplicate consecutive same-sender writes.
  • Auto-scroll threshold preserves reading position and permission-row cursor.
  • Thinking state clears on non-thought writes.
Install via CLI
npx skills add https://github.com/carlos-algms/agentic.nvim --skill agentic-ui-message-writer
Repository Details
star Stars 514
call_split Forks 47
navigation Branch main
article Path SKILL.md
More from Creator
carlos-algms
carlos-algms Explore all skills →