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
wrapstays on.- Cursor positioning is
G0zb, notG$zb. - Cursor sits on the trailing
""line below the last block, never inside a tool-call block. scrolloff = 4on 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.schedulebetween 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_countis 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 * Nfor N permission options.- Permission rows are outside the fold.
MessageWriter:_render_permission_sectionowns 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_padand 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_chunkorwrite_tool_call_block. replay_history_messagesdoes not re-issue ACPsend_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
TodoListownsChatWidget.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.schedulebetween mutation andG0zb: redraw can run with stale topline.- Replacing the whole tool-call range: manual fold dies.
- Re-rendering tool-call body after
tracker.diffexists: 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.