Conversation
Shift+Enter scrambled multi-line messages: `one`, Shift+Enter, `two`, Shift+Enter, `three` submitted as `onetwothree\n\n`. The issue suggests routing Shift+Enter through "the cursor-aware insert Ctrl+J already uses", but no such path existed — both branches in user-input.tsx ran the same `updateInput(input + '\n')`, appending to the END of the value. The real asymmetry is in TextInput, and it follows from how Ink reports the keys: Ctrl+J arrives as a literal LF (`name:'enter'`, `return:false`) and fell into the generic insert, which places it at the cursor; Shift+Enter arrives as `name:'return'` with `shift:true` and hit `if (key.return)`, which swallowed it outright — no insert and, crucially, no cursor move. So for Shift+Enter only the parent's end-append ran, leaving the caret behind, and every later keystroke was spliced in at the stale offset. Ctrl+J was broken too, contrary to the issue. It only looked correct because the caret is usually at the end, where the parent's append happens to compute the same string. With this fix reverted, the Ctrl+J test yields `onetwo\n` rather than `one\ntwo`. TextInput owns the cursor, so it now owns newline insertion: - `insertAtCursor` moves to utils/text-wrapping.ts, beside `moveCursorToVisualLine`, and backs all three insert paths — a typed character, Ctrl+J and Shift+Enter — so there is one implementation instead of three. It also keeps the helper out of a module that imports ink, which the spec cannot load. - Shift+Enter falls through to the edit chain instead of being swallowed. - Ctrl+J gains a `case 'j'` for the kitty-protocol form, which previously fell to `default:` and was ignored. - The parent's two end-appending branches are deleted. That redundant append was the bug. Shift+Enter now inserts a line break in every TextInput, not only the composer. TextInput's generic insert already accepted a literal LF everywhere, so this makes the CSI-u and kitty encodings consistent with behaviour the component already had; gating only the new encodings would leave Ctrl+J-as-LF inserting while kitty Ctrl+J did not, in the same field. Tests drive real CSI-u bytes through stdin and assert the submitted message: the exact sequence from the report, plus a Ctrl+J regression guard. Both fail without the fix and reproduce `onetwothree\n\n`. `insertAtCursor` is covered directly — the real export, not a re-implementation as the older tests in that file use — across caret at start, middle and end.
Patch, per the PR template's changeset gate. The entry describes the composer behaviour change for the changelog, including the note that Shift+Enter now inserts a line break in every text input rather than only the composer.
nc-review: comments — 1 nit@addyCooks — a few things worth a look, none blocking. The PR correctly diagnoses and fixes the Shift+Enter scramble bug: the parent was appending '\n' to the end of the value without moving the caret, so subsequent keystrokes were spliced in at the stale offset. The fix moves newline insertion into TextInput (which owns the cursor) via a new shared ⚪ nit · Shift+Enter now inserts a line break in every 🔴 blocking · 🟠 a reviewer would ask for a change · ⚪ optional Automated code review — correctness, security, design, tests, plus duplicates and scope. A human still decides; this is not a substitute for review and is not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with |
The coverage gate failed on a 0.01% drop against main. The cause was the new `case 'j'` in TextInput: it handles the kitty keyboard protocol's encoding of Ctrl+J, which no test sent, so every statement in the branch was added uncovered. A literal LF takes the generic-insert path instead and never reaches it. The Ctrl+J test now drives both encodings, which is what the branch exists for and what its name should have claimed. `case 'j'` goes from entirely uncovered to fully covered, and text-input.tsx from 59.21% to 63.13% of statements. Every other line this branch touches was already covered; the lines still uncovered around it (the arrow-key and readline-keybind branches) predate it.
Description
Shift+Enter scrambled multi-line messages:
one, Shift+Enter,two, Shift+Enter,threesubmitted asonetwothree\n\n.The issue suggests routing Shift+Enter through "the cursor-aware insert Ctrl+J already uses", but no such path existed, both branches in
user-input.tsxran the sameupdateInput(input + '\n'), appending to the END of the value. The real asymmetry is inTextInput, and it follows from how Ink reports the keys: Ctrl+J arrives as a literal LF (name:'enter',return:false) and fell into the generic insert, which places it at the cursor; Shift+Enter arrives asname:'return'withshift:trueand hitif (key.return), which swallowed it outright, no insert and, crucially, no cursor move. So for Shift+Enter only the parent's end-append ran, leaving the caret behind, and every later keystroke was spliced in at the stale offset.Ctrl+J was broken too, contrary to the issue. It only looked correct because the caret is usually at the end, where the parent's append happens to compute the same string. With this fix reverted, the Ctrl+J test yields
onetwo\nrather thanone\ntwo.TextInputowns the cursor, so it now owns newline insertion:insertAtCursormoves toutils/text-wrapping.ts, besidemoveCursorToVisualLine, and backs all three insert paths — a typed character, Ctrl+J and Shift+Enter, so there is one implementation instead of three. It also keeps the helper out of a module that imports ink, which the spec cannot load.case 'j'for the kitty-protocol form, which previously fell todefault:and was ignored.Shift+Enter now inserts a line break in every
TextInput, not only the composer.TextInput's generic insert already accepted a literal LF everywhere, so this makes the CSI-u and kitty encodings consistent with behaviour the component already had; gating only the new encodings would leave Ctrl+J-as-LF inserting while kitty Ctrl+J did not, in the same field. Flagging it here in case you'd prefer the narrower scope.Closes #1326.
Recording
Type of Change
Changeset
pnpm changeset) describing this change for the changelogTesting
Automated Tests
.spec.ts/tsxfilespnpm test:allcompletes successfully)Manual Testing
Checklist