Skip to content

fix(claude): confirm inlined pastes on the runtime delivery path (remote send bug) - #498

Merged
Juliusolsson05 merged 1 commit into
mainfrom
fix/claude-paste-inline-confirm
Jul 8, 2026
Merged

Juliusolsson05 merged 1 commit into
mainfrom
fix/claude-paste-inline-confirm

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Fixes the never-ending "remote/mobile send doesn't work" bug (Claude session … did not confirm pasted prompt before submit (timeout)), root-caused with a real-claude PTY harness rather than guessed.

Root cause (verified, not assumed)

Claude's TUI reflects a committed bracketed paste two ways, by size:

  • COLLAPSE → [Pasted text #N] placeholder — only big pastes (single-line >800 chars, multiline ≥4 lines).
  • INLINE → raw text, no placeholder — everything smaller.

The runtime delivery path (deliverClaudePrompt) waited for the placeholder only (awaitPastePlaceholder). So every short/inlined paste timed out at 2000ms, Enter was withheld, and the prompt stuck in the composer. This hits every short remote prompt — and every dictated prompt, since the <stt>…</stt> wrapper adds newlines and forces the paste route.

It's a drift bug: the desktop composer already confirmed on placeholder OR inline-tail (claudePaste.ts), but that robust detector was never ported to the runtime path.

Measured against real claude (harness in tmp/paste-repro/):

paste collapse? old (placeholder-only) fix (placeholder-or-inline)
single 120ch inlined ⏱ timeout → stuck ✅ inline @22ms
multiline 2-line inlined ⏱ timeout → stuck ✅ inline @24ms
dictated <stt> short inlined ⏱ timeout → stuck ✅ inline @11ms
single 2000ch collapsed ✅ placeholder ✅ placeholder

The fix

  • New shared detector @shared/claude/pasteConfirm — placeholder-count-increase or the paste's normalized 24-char tail newly appearing, against a baseline captured before the paste (the baseline also makes a second paste immune to the first's stale [Pasted text #1]).
  • deliverClaudePrompt reads the live screen via a new AgentSession.snapshotScreen (Claude already implements it) — app-only, no submodule change.
  • claudePaste.ts now imports the shared detector (byte-identical logic; the battle-scarred timing constants untouched). Duplicated CLAUDE_PASTE_THRESHOLD / isPasteLike collapse to one source, so the two paths can't drift again — which is exactly why this bug existed.

Verification

  • tsc -b tsconfig.node.json clean, tsc --noEmit -p tsconfig.web.json clean.
  • promptDelivery.test.ts updated to the snapshotScreen mechanism: 6/6 pass, pinning the inline-tail path, the stale-placeholder sequence guard, and Enter-withheld-on-timeout.
  • End-to-end against real claude: the actual shared pollPasteAbsorbed resolves absorbed for every dead-zone case (short single-line, short multiline, dictated <stt>) — the exact prompts that used to hang.

Independent of PR #497 (mobile UI); branched off clean main.

🤖 Generated with Claude Code

…ote send)

The never-ending "remote/mobile send doesn't work" bug. Claude's TUI reflects a
committed bracketed paste two ways depending on size: it COLLAPSES big pastes
into a `[Pasted text #N]` placeholder (single-line >~800 chars, multiline >=~4
lines) but INLINES everything smaller with no placeholder at all. The runtime
delivery path (deliverClaudePrompt) waited for the placeholder ONLY
(awaitPastePlaceholder), so every short/inlined paste timed out unconfirmed
("did not confirm pasted prompt before submit") and the message stuck in the
composer, unsent. That hits every short remote prompt — and EVERY dictated
prompt, since the <stt>…</stt> wrapper adds newlines and forces the paste
route.

Root cause is drift: the desktop composer already confirmed on placeholder OR
inline-tail (claudePaste.ts waitForPasteAbsorbed), but that robust detector was
never ported to the runtime path. Fix consolidates the detection into a shared
module (@shared/claude/pasteConfirm) used by BOTH paths, so it can't drift
again:
 - new placeholder count increase OR the paste's normalized 24-char tail
   newly appearing, against a baseline captured BEFORE the paste (the baseline
   also makes a SECOND paste immune to the first's stale [Pasted text #1]).
 - runtime reads the live screen via a new AgentSession.snapshotScreen (Claude
   already implemented it as snapshotScreen()); app-only, no submodule change.
 - claudePaste.ts now imports the shared detector (byte-identical logic; the
   battle-scarred timing constants are untouched); the duplicated
   CLAUDE_PASTE_THRESHOLD / isPasteLike collapse to one source.

Characterized and verified against a real `claude` PTY (harness in
tmp/paste-repro): short single-line, short multiline, and dictated <stt>
prompts all now confirm via the inline signal in ~11-24ms (were 2000ms
timeouts); big pastes still confirm via placeholder. Unit test updated to the
snapshotScreen mechanism and pins the inline-tail path + the stale-placeholder
sequence guard + Enter-withheld-on-timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 7c449f4 into main Jul 8, 2026
@Juliusolsson05
Juliusolsson05 deleted the fix/claude-paste-inline-confirm branch July 8, 2026 12:31
Juliusolsson05 added a commit that referenced this pull request Jul 9, 2026
…ack + #498 paste fix)

One conflict, SessionView.tsx imports: union of this branch's chrome
imports (PaneHeader/ComposerInput/useComposerAutoGrow) with main's
post-#493 paths — useLedgerFeedItems now lives in features/feed/ledger,
the runtime view types against RuntimeRenderInput from
session-runtime/state (the SessionRuntime cast died in #502), and
NO_GHOSTS is ReadonlyMap<string, GhostEntry>. The file body auto-merged
main's honestly-typed runtimeView (semantic: transcript.semantic) under
this branch's chrome unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant