Skip to content

fix(input): deliver bracketed pastes to every text field - #1501

Open
addyCooks wants to merge 4 commits into
Nano-Collective:mainfrom
addyCooks:fix/paste-all-text-fields-1456
Open

addyCooks wants to merge 4 commits into
Nano-Collective:mainfrom
addyCooks:fix/paste-all-text-fields-1456

Conversation

@addyCooks

@addyCooks addyCooks commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1456 (part of #907)

Description

cli.tsx enables bracketed paste and lifts each payload off stdin before Ink's keypress parser sees it, then republishes it on the pasteEvents bus. Only UserInput subscribed to that bus, so every other text field (the MCP and provider wizards, the settings panels, the ask_user prompt, the architect review's revise box) got neither the paste event nor the raw keystrokes, and a paste into any of them was silently dropped.

The subscription now lives in the shared TextInput, as the issue suggests:

  • It subscribes while focus is true, the same condition that gates its useInput. A paste goes to exactly the field that typing would reach, and never to more than one.
  • By default it inserts the payload at the caret, turning the terminal's CR line breaks into \n, and leaves the caret after the pasted text.
  • A new optional onPaste prop replaces that default. UserInput passes its existing handler through it, so the composer keeps its placeholder collapsing for long pastes and caret-aware splicing, and it no longer subscribes itself.
File Change
source/components/text-input.tsx Subscribe to pasteEvents while focused; insert at the caret or hand off to onPaste
source/components/user-input.tsx Drop its own subscription; pass the same handler as onPaste
source/utils/terminal-paste.ts Bus comment now names the focused TextInput as the subscriber
source/components/text-input-keys.spec.tsx Paste at caret, CR line breaks, unfocused field ignores it, onPaste override
source/wizards/steps/field-input-view.spec.tsx Paste reaches a wizard field, plain and masked

Recording

B34-FIXED-paste-all-fields

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Changeset

  • Added a changeset (.changeset/paste-all-text-fields.md, patch)

Testing

  • 5 new tests. 4 fail on main (paste at the caret, CR line breaks, the onPaste override, and the wizard field). The fifth, "unfocused field ignores a paste", guards the new subscription and passes on main because nothing received pastes there.
  • text-input-keys 20/20 and field-input-view 15/15. All five existing composer paste tests in user-input.spec.tsx pass through the new path (placeholder collapse, literal short paste, ignored while disabled, caret parked after the splice).
  • tsc, Biome format and lint, and knip clean

Note

Pasted line breaks are kept in every field, as in the composer, so a trailing newline copied along with an API key ends up in the value. Trimming it in single-line fields would mean deciding per field, which felt out of scope here. Happy to follow up if you want it.

Checklist

  • I was assigned to the issue
  • Code follows project style guidelines
  • Self-review completed
  • No breaking changes

cli.tsx lifts bracketed-paste payloads off stdin before Ink sees them and
republishes them on pasteEvents, but only UserInput subscribed. Every other
TextInput (MCP and provider wizards, settings panels, ask_user, the
architect revise box) got neither the event nor the raw keystrokes, so a
paste into any of them was silently dropped.

TextInput now subscribes while focused, the same condition that gates its
keystrokes, and inserts the payload at the caret with CR line breaks
turned into newlines. UserInput passes its existing handler as onPaste, so
the composer keeps its placeholder collapsing and there is still a single
subscriber per focused field.

Closes Nano-Collective#1456
@github-actions

Copy link
Copy Markdown
Contributor

nc-review: comments — 2 important, 2 nits

@addyCooks — a few things worth a look, none blocking.

Moves the bracketed-paste subscription from UserInput into the shared TextInput so every text field in the app receives pastes. The design correctly preserves the composer's placeholder-collapsing behaviour via a new optional onPaste prop, and the focus gate ensures exactly one field consumes each paste. Tests cover caret insertion, CR→
conversion, the focus gate, the onPaste override, and a wizard field. One real concern and a couple of nits are listed below.

🟠 important · design · source/components/user-input.tsx

The pre-change useEffect that subscribed to pasteEvents was also gated on disabled (if (disabled || !effectiveFocus) return). The new design drops that disabled check and only consults focus, which works today because UserInput short-circuits to a Spinner branch when disabled=true and never mounts TextInput — so no listener is registered. The assumption is implicit, though: if a future refactor keeps TextInput mounted (e.g. with focus={false}) while disabled, the gate stays correct, but if it keeps TextInput mounted with focus={true} for any reason (a future disabled-while-editable mode, or forceFocus in a test), pastes will reach the disabled composer. Worth either documenting the assumption near the new useEffect or routing disabled through the focus prop so the gate stays explicit at the receiving component.

🟠 important · correctness · source/components/text-input.tsx:154

useImperativeHandle is declared with an empty dep array ([]) further up. The new paste-subscription useEffect correctly depends only on focus, but handlePasteRef.current = (payload) => {…} runs outside any effect, on every render including the very first. That is intentional and fine here — but the immediate reassignment next to const handlePasteRef = useRef(() => {}) looks like a copy/paste hazard. A short comment ('assigned on every render so the listener always sees the latest onPaste') would keep the next reader from 'fixing' it by moving the assignment into a useEffect, which would re-introduce the very stale-closure bug the ref pattern is there to avoid.

⚪ nit · tests · source/components/text-input-keys.spec.tsx

The new suite asserts the happy path (insert at caret, CR→\n, onPaste override, focus gate) and the wizard integration, but there is no test that exercises the cleanup path: e.g. rendering a focused TextInput, firing a paste, then unmounting and asserting pasteEvents.listenerCount('paste') returns to its baseline. With the singleton bus and four concurrent paste-using specs, a missing off would silently leak a listener across tests and intermittently double-fire handlers in the serial AVA suite. A one-line t.is(pasteEvents.listenerCount('paste'), 0) after unmount() would close that gap.

⚪ nit · warranted

The change is fully justified — the issue describes a concrete, reproducible bug (paste silently dropped in every non-composer field) with a clear suggested fix at exactly the location this PR modifies, and the diff implements that fix with regression tests in text-input-keys.spec.tsx and an end-to-end test in field-input-view.spec.tsx. No reimplementation of existing work, no churn.


🔴 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 /re-review.

@github-actions github-actions Bot added the agent:comments nc-review left non-blocking findings label Sep 25, 2026
- Fold `disabled` into the composer's TextInput focus, so the paste gate
  is explicit at the receiving component instead of relying on the
  disabled branch never mounting it.
- Explain why handlePasteRef is reassigned on every render rather than in
  an effect.
- Test that the paste listener is removed on blur and on unmount.
…fields-1456

# Conflicts:
#	source/components/text-input-keys.spec.tsx
#	source/components/user-input.tsx
@addyCooks

Copy link
Copy Markdown
Collaborator Author

Hey @akramcodez,
can you pls review this one : )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:comments nc-review left non-blocking findings area:tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Bracketed paste only works in the main prompt, every other text field in the app silently receives nothing

1 participant