Skip to content

feat: store large/multiline pastes behind a placeholder token (#53)#67

Merged
yasha-dev1 merged 1 commit into
mainfrom
feat/large-paste-placeholder
Jun 16, 2026
Merged

feat: store large/multiline pastes behind a placeholder token (#53)#67
yasha-dev1 merged 1 commit into
mainfrom
feat/large-paste-placeholder

Conversation

@yasha-dev1

Copy link
Copy Markdown
Collaborator

Closes #53. (Related: #60 — see Scope.)

Problem

A large/multi-line paste was inserted inline into the textarea's single logical line, with newlines flattened to spaces (input.ts Ctrl+V handler). A big paste therefore soft-wrapped across many rows and broke the input frame, footer, completion panel, and caret math — and the original newlines were lost.

Fix

Keep the raw text in a per-textarea paste store and render a compact placeholder token in the buffer, expanding it back to the exact original text only at submit.

  • cli/paste-store.ts (new, pure & unit-tested):
    • shouldStorePaste(text) — store on any newline or > 200 chars; inline otherwise.
    • register(text)[Pasted text #N +L lines] (or … C chars] for a long single line).
    • expand(value) — replaces every known token with its raw text; unknown look-alikes are left as typed.
    • tokenEndingAt(value, pos) — for atomic deletion.
    • clear() — ids stay monotonic so a stale token in scrollback never collides with a fresh entry.
  • cli/input.ts:
    • Ctrl+V stores large pastes (placeholder inserted) and inlines small ones (unchanged).
    • Backspace at a placeholder's right edge removes the whole token and frees its entry.
    • Submit expands placeholders (newlines preserved) and clears the store.

Acceptance criteria

  • Pasting a large multiline block shows a placeholder and doesn't distort the input box (the buffer holds a short token, not the raw rows).
  • Submitting expands the placeholder to the original full text (newlines preserved).
  • Deleting the placeholder token removes the associated stored content.
  • Small/short pastes behave as before.

Scope

This covers the Ctrl+V / onPaste path (the deterministic, testable entry point). Capturing raw terminal bracketed paste — the path behind the IntelliJ-terminal hang in #60, where readline turns pasted newlines into repeated submits — requires stdin/bracketed-paste handling and is a focused follow-up; I didn't want to bundle that less-testable terminal work into this PR. #60 is therefore not closed here.

Tests

  • cli/tests/paste-store.test.ts — 14 unit tests (thresholds, placeholder formatting incl. singular/plural and char-count, single/multi-token expansion, unknown-token passthrough, tokenEndingAt positive/negative, clear + monotonic ids).
  • cli/tests/textarea-paste.test.ts — integration via the real createTextarea + fake stdin: multiline round-trip with newlines preserved, placeholder embedded among typed text, single-backspace atomic delete (proves it's one token, not raw chars), short paste still inlined. Updated the prior newline-flatten test to the new preserve-newlines behavior.

🤖 Generated with Claude Code

A large or multi-line paste was inserted inline into the single-line
textarea buffer (newlines flattened to spaces), which exploded the input
across many wrapped rows and mangled the frame/caret math — and lost the
original newlines.

Now a paste above a threshold (any newline, or > 200 chars) is kept in a
per-textarea paste store and represented by a compact placeholder token
(`[Pasted text #1 +12 lines]`), expanded back to the exact original text
(newlines preserved) at submit. Small single-line pastes inline as before.

- paste-store.ts (new, pure/unit-tested): shouldStorePaste threshold,
  register → placeholder, expand (multiple tokens; unknown look-alikes left
  as typed), tokenEndingAt for atomic deletion, clear (ids stay monotonic).
- input.ts: Ctrl+V stores large pastes; backspace at a token's right edge
  deletes the whole token (and frees its entry); submit expands placeholders
  and clears the store.

Scope: the Ctrl+V/onPaste path (deterministic). Capturing raw terminal
bracketed paste (the path behind the IntelliJ-terminal hang, #60) needs
stdin-level work and is a follow-up.

Tests: 14 unit (paste-store) + 4 textarea integration (multiline round-trip
with newlines preserved, embedded placeholder, single-backspace atomic
delete, short paste still inlined); updated the prior newline-flatten test
to the new preserve-newlines behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yasha-dev1

Copy link
Copy Markdown
Collaborator Author

✅ Verification

Full suite + typecheck + lint (clean)

Test Files  73 passed (73)
     Tests  646 passed (646)
  • npm run build (core + cli) → success
  • npm run typecheck → pass
  • npm run lint → pass

New/updated tests for this feature (21 across the two files)

✓ packages/cli/tests/paste-store.test.ts   (14 tests)
✓ packages/cli/tests/textarea-paste.test.ts (7 tests)

Behavior confirmed (integration, real createTextarea + fake stdin)

  • Multiline paste round-trips: Ctrl+V a 3-line block → submit emits the original text with \ns intact (no longer flattened).
  • Embedded placeholder: see + paste + ok → submit emits see <original> ok.
  • Atomic deletion: after pasting a 5-line block, one backspace clears it entirely → submit emits '' (proves the buffer held a single placeholder token, not ~9 inlined chars).
  • Small paste unchanged: short paste + one backspace → submit emits short past (inlined, char-by-char as before).

Shipped-binary check

$ grep -c "Pasted text #" packages/cli/dist/index.js
3

Note on #60

This fixes the Ctrl+V/onPaste path. The IntelliJ-terminal infinite-loop in #60 comes from raw bracketed paste (readline turning pasted newlines into repeated submits), which needs stdin-level handling — tracked as a follow-up, not closed by this PR.

@yasha-dev1 yasha-dev1 added the PR OK Grok PR verified and review passed label Jun 16, 2026
@yasha-dev1

Copy link
Copy Markdown
Collaborator Author

PR OK — verified end-to-end (posted as comment; GitHub blocks self-approval).

Addresses #53: large/multi-line Ctrl+V pastes are now kept in a per-textarea paste store and shown as a compact [Pasted text #N +L lines] token, expanded back to the exact original at submit. All four acceptance criteria met.

Verified the integration seams in input.ts:

  • Ctrl+V branches on shouldStorePaste(text) (any newline or >200 chars) → register else inline-flatten; small pastes unchanged.
  • Submit computes value = pasteStore.expand(buffer.trim()) before pasteStore.clear() — correct order, and newlines survive because trim() applies to the token-bearing buffer, not the expanded raw text.
  • Backspace at a token's right edge deletes the whole token and frees its entry via tokenEndingAt; entries.has(id) guards against grabbing a user-typed look-alike.
  • clear() keeps ids monotonic so a stale token in scrollback can't collide with a fresh entry.

Scope is honestly documented: this covers only the deterministic onPaste/Ctrl+V path; raw bracketed-paste (#60) is explicitly left as a follow-up and #60 is not closed here. Correct call — bundling the less-testable terminal work would have muddied this PR.

Verification: build ✓ · typecheck ✓ · lint ✓ · full suite 646/646 · 21 new tests — 14 unit (thresholds, singular/plural & char-count formatting, single/multi expansion, unknown-token passthrough, tokenEndingAt ±, monotonic-id clear) + 7 integration via real createTextarea + fake stdin (multiline round-trip newlines-preserved, embedded-among-typed-text, single-backspace atomic delete, short paste still inlined).

Pure, well-tested core + correct wiring. LGTM.

@yasha-dev1 yasha-dev1 merged commit 5317f6d into main Jun 16, 2026
2 checks passed
@yasha-dev1 yasha-dev1 deleted the feat/large-paste-placeholder branch June 16, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR OK Grok PR verified and review passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Paste: store large multiline pastes internally and show a placeholder in the textarea

1 participant