PR #228 review found a correctness gap this lane made reachable. The
GPU dropdown derives its height, its visible window and its
selection-highlight offset from `rows.len()` — ONE logical row per
candidate — while a detail carrying a line break shapes into more
physical lines than that. One such row misaligns every row below it
and the highlight with it. The grid TUI has the same exposure from the
other side: it writes the description into a single-row suffix on the
minibuffer band.
## Why not reject CR/LF at registration
That was the obvious fix. It was implemented, measured, and abandoned
on evidence.
MCP tool registration renders a whole schema block into
`Command.description` — tool text, blank line, `Arguments:`, then one
line per argument (`tests/fixtures/pmacs-mcp-tools/init.lua:272`, a
`table.concat(lines, "\n")`, used at `:496`). And
`tests/m9_6_acceptance.rs:583-598` ASSERTS four of those lines. A
one-line guard in `CommandRegistry::define` fails 36 tests across
`m9_6` (19/25), `m9_7` (16/19) and `m9_8` (1/17), in both feature
configurations, and could only be made green by deleting a shipped
acceptance criterion.
So the one-line constraint goes where the constraint actually is: the
surfaces that have one row. `Command.description` stays free-form,
which it legitimately is.
## The change
`Command::description_first_line` clips to the first CR **or** LF — a
lone CR ends a line too, and an LF-only clip would pass a bare `\r`
straight through to the same surface. Both single-row consumers call
it: the semantic producer filling `MinibufferRow.detail`
(`src/semantic_render.rs`) and the TUI suffix (`src/editor.rs`). A
first line that is empty ships as `None` rather than `Some("")`, which
would draw trailing padding.
No ellipsis or truncation marker, matching the in-tree precedent and
the minibuffer's own width rule.
`describe-command` and `help.list-commands` are untouched and still
report every line. That is what makes this a rendering decision rather
than data loss, and it is asserted, not assumed.
## Precedent, already in this tree
The same MCP fixture clips a tool RESULT to its first line because
"a multi-line set_status would corrupt the row layout"
(`init.lua:277-285`), leaving width clipping to the frontend. Same
hazard class, same resolution.
## Verification
`src/command.rs`: a schema block registers AND clips, in all three
break forms; a single-line description is byte-identical after the
clip; an empty first line clips to empty.
`tests/discovery_stage2_acceptance.rs`: an MCP-shaped description
reaches the TUI band and the GPU row as one line, through the real
prompt path — with the full text still reachable via
`describe-command` asserted alongside, so a clip that deleted the
schema block everywhere would fail rather than pass.
`pmacs-gpu`: one physical shaped line per logical candidate row — the
geometry invariant the dropdown depends on.
Mutation-checked: neutering `first_line` to the identity fails all
four new break-handling tests
(`a_multi_line_description_registers_and_clips_to_its_first_line`,
`a_description_whose_first_line_is_empty_clips_to_empty`,
`a_multi_line_description_reaches_the_tui_band_as_one_line`,
`a_multi_line_description_reaches_the_gpu_row_as_one_physical_line`)
and leaves the two "did not tighten past purpose" tests green.
`Command.description`'s doc comment claimed "one-line", which the MCP
path openly violates. It now states the real contract and records why
a registration guard must not be re-proposed.
`m9_6`/`m9_7`/`m9_8` pass COMPLETELY UNTOUCHED, and are now named
gate suites so that stays on the record.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai