# Agentic UI Message Writer

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

- Skill: `carlos-algms/agentic-ui-message-writer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add carlos-algms/agentic-ui-message-writer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/carlos-algms/agentic-ui-message-writer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: carlos-algms (https://skillmd.com/u/carlos-algms)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/carlos-algms/agentic-ui-message-writer

---


# 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

```text
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.

```text
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.

