Repository navigation
fix(claude): confirm an inline paste whose tail the TUI hard-wrapped - #1219
Merged
Merged
Conversation
Claude's Ink wraps the composer with hard: true, so a token longer than the line (an absolute path, a URL) is cut mid-token onto an indented continuation line. The inline-tail detector collapsed whitespace, which turned that cut into a space inside the path, so the tail needle never matched: a paste-like prompt ending in a long path timed out after 5 s and rolled back at 14 of 81 pane widths. The tail comparison now strips whitespace on both sides, which is blind to where either kind of wrap falls; the placeholder counters keep collapsing, since their regexes need the spaces. Test: the composer chrome of a recorded Claude 2.1.278 frame, re-rendered at every width 20-140 with Ink's own wrap options. It fails on the old code at 20+ widths (including the issue's 46-50), and a second case keeps a half-landed paste unconfirmed. Fixes #1118 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The width-sweep test imported wrap-ansi, a hoisted transitive dependency with no types, so tsc -b failed. It is now a declared devDependency (the lockfile's existing hoisted 7.0.0 entry, dev: true; only the root entry changes) and is loaded through a typed require, since v7 is CommonJS with no declarations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er missing whitespace Review round 1 on #1219 (2 Codex reviewers; the Pi report is pending): - Major (A): stripping every whitespace character also accepted MISSING whitespace, so a payload foo\nbar 'landed' on a composer showing foobar, and the recorded unrelated 'opencode [Image #1]' composer matched a pending opencode[\nImage#1] payload. Both sent Enter for a paste that had not landed. The tail is now matched by a pattern: each whitespace run in the (collapsed) needle must be whitespace on screen, and between any other two characters the screen MAY hold whitespace, which is exactly what a soft or hard wrap inserts. - Major (B): the baseline transition had no test; dropping the tailAlreadyPresent guard or reverting the baseline to a plain substring survived. A second paste over an already-present hard-wrapped tail now times out instead of confirming. Tests: both reviewer reproductions and the baseline case. Each fails under its mutation (space made optional; guard removed; baseline without wrap tolerance), and the 20-140 column sweep still passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t fell Review round 2 (A) on #1219 found that the round-1 pattern, which allowed optional whitespace between any two characters, matched an EARLIER part of the same paste that had a real space where the tail has none, and so sent Enter before the tail arrived. It also read the recorded soft-wrapped '[Image' / ' #1]' pill as 'Image#1'. Both withdrawn approaches guessed where a wrap might be. The composer text is now reconstructed: Claude draws its chrome divider across the full composer width, so a line that fills that width and whose continuation does not start with a space was cut mid-token and joins with nothing between; every other break was a soft wrap and joins with one space. The tail is then a plain substring of that text, for both baseline and poll. With no divider on screen every break is treated as soft (the old behaviour), and misreading a rare exactly-full soft line errs toward a timeout, never an early Enter. Tests: both round-2 reproductions, plus the round-1 cases and the 20-140 column sweep, now through activeClaudeComposerText as production uses it. Mutation-checked: never-hard, width ignored, physical baseline, and baseline guard removed each turn a test red. Final round: fixed by the author per the at-most-two-rounds rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pi reviewer calibrated npm wrap-ansi against both recorded frames (Claude Code 2.1.278, 64 columns): the composer body is pane - 3 columns, the prompt marker is U+276F + NO-BREAK SPACE, so a hard-wrapped line is 63 wide, one column less than the 64-column divider. The reconstruction compared against the divider width itself, so on a real screen it would never have detected a hard cut; the tests passed only because their synthetic geometry (pane - 2, plain space) did not match the recording. The test now renders with the recorded geometry, and fails with the old threshold (verified), and the threshold is divider width - 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a Claude prompt is paste-like but not collapsed into a placeholder, Agent Code proves it landed by finding the prompt's last 24 characters in the composer (
pasteTailNeedle,pasteAbsorbedViaandpollPasteAbsorbedinsrc/shared/claude/pasteConfirm.ts). Claude's Ink wraps withhard: true(vendor/claude-code-src/full/ink/wrap-text.ts), so a token longer than the line (an absolute path, a URL) is cut mid-token. Collapsing whitespace turned that cut into a space the needle does not contain. The send hit its 5 s budget, rolled back, and left the prompt in the composer. This is #1113's experience on the text path.Change
activeClaudeComposerTextreconstructs the text the composer holds, undoing the TUI's wrapping. Claude draws its divider across the full width, and a composer line is one column shorter (measured on the recordings). A line that fills the composer width and whose continuation doesn't start with a space was cut mid-token, so the two join directly; every other break is a soft wrap and joins with a space. The tail is then a plain substring of that text, for both the baseline and every poll. With no divider on screen, every break is treated as soft (the old behaviour). A misread errs toward a timeout, never an early Enter.Two earlier approaches were withdrawn in review: stripping all whitespace (it accepted missing whitespace) and a pattern with optional whitespace everywhere (it matched earlier text). Both guessed where the wrap was.
wrap-ansibecomes an explicit devDependency. The lockfile already had it hoisted withdev: true; only the root entry changes. The test loads it through a typedrequire.Tests (fail-first)
testing/fixtures/image-absorption). The composer body is re-rendered at each width with Ink's own wrap options, for a prompt ending in…/promptDelivery.ts. The old code misses 20+ widths, including the issue's 46–50.foo\nbardoes not confirm on a composer showingfoobar, and does confirm onfoo bar.opencode [Image #1]composer does not confirm a pendingopencode[\nImage#1].Mutation-checked, each individually: whitespace made optional, baseline guard removed, and baseline without wrap tolerance each turn a test red. Collapse instead of the pattern fails the sweep.
Review (2 Codex + 1 Pi)
foo\nbarconfirms onfoobar, and a recorded unrelated image composer confirms a pending payload, so Enter is sent early696578fa(asymmetric pattern), both are teststailAlreadyPresentor reverting the baseline comparison survived696578fa, test over a hard-wrapped pre-existing tailtsc -bfailed on an untypedwrap-ansiimport6ef099f5| A round 2: optional whitespace between ANY characters matched an earlier part of the same paste (and read the recorded
[Image/#1]asImage#1) | valid, major | fixed in04db26dc: the composer text is reconstructed (a full line joins a mid-token cut directly, any other break is a soft wrap and joins with a space); both are tests || Pi F3: test geometry was pane − 2; the recordings are pane − 3 with
❯+ NBSP, so a hard-wrapped line is one column short of the divider, and the threshold would never have fired on a real screen | valid, major | fixed in73df03a6: geometry calibrated on the recordings, threshold = divider − 1, and the test fails on the old threshold || Pi F1 (baseline guard untested), F2 (needle length unpinned) | valid, minor | F1 covered by the baseline test; F2 accepted (24 is a distinctiveness tuning, not a contract) |
Rounds: 2 (Codex A, Codex B, Pi). Round-2 findings were fixed by the author without a third round, per the at-most-two-rounds rule. Final state: B MERGE-READY, Pi MERGE-READY, A's round-2 finding fixed and tested.
Verification
npx tsc -bclean.src/shared/claude,TileLeafandsrc/providers/claudepass.Not verified here: a live Claude pane. Claude runs
Bun.wrapAnsi, not the npmwrap-ansi; both implementhard.Fixes #1118
🤖 Generated with Claude Code