Skip to content

Copying a chat text selection pastes invisible trailing newlines #2523

Description

@hemaaanth

Summary

Selecting an excerpt from an assistant message, copying it, and pasting it into the composer can insert several blank lines after the visible text. The clipboard should contain only the text the user can see selected.

Versions and environment

  • bb 0.40.0, reproduced from source at 31a190ddb5f0ffb86cb8dcd80482497663de2685 (origin/main)
  • Arch Linux, kernel 7.1.8-arch1-3, x86_64
  • Node.js v26.7.0
  • Chromium 152.0.0.0
  • Clean development data directory and detached checkout of origin/main
  • Provider-independent after the assistant message exists; Codex was used only to create the fixture message

Steps to reproduce

  1. Start bb from main with pnpm dev and open any thread containing an assistant paragraph followed by another timeline row.
  2. In the assistant paragraph, double-click and drag from a word near the start through the visually final word of the paragraph.
  3. Press Ctrl+C.
  4. Click the composer and press Ctrl+V.
  5. Observe the blank lines inserted after the selected sentence.

A deterministic DevTools-console helper can create the same cross-row selection before steps 3-4:

(() => {
  const prose = [...document.querySelectorAll("[data-sidebar-swipe-selectable]")]
    .find((node) => node.textContent?.trim());
  const textNode = prose?.querySelector("p")?.firstChild;
  const row = prose?.closest("[data-timeline-row-id]");
  const followingRow = row?.nextElementSibling;
  if (!(textNode instanceof Text) || !(followingRow instanceof Element)) {
    throw new Error("Need an assistant paragraph followed by another timeline row.");
  }
  const range = document.createRange();
  range.setStart(textNode, Math.min(3, textNode.length));
  range.setEnd(followingRow, 0);
  const selection = window.getSelection();
  selection?.removeAllRanges();
  selection?.addRange(range);
  const text = selection?.toString() ?? "";
  console.log({
    text: JSON.stringify(text),
    trailingWhitespace: text.length - text.trimEnd().length,
  });
})();

Browser security requires Ctrl+C and Ctrl+V to remain trusted user actions, so a fully self-contained console script cannot perform the final clipboard steps.

Expected vs actual

Expected:
The composer contains the visibly selected sentence and no trailing blank lines.

Actual:
The native selection and text/plain clipboard payload contain the sentence followed by seven newline characters. Pasting expands the composer with seven blank lines after the sentence. Submitting the message strips those trailing newlines, so the sent message is unaffected.

Evidence

  • Reproduced on main at 31a190ddb5f0ffb86cb8dcd80482497663de2685.
  • The visible selection contained one sentence, while the native selection and text/plain clipboard payload ended with seven newline characters.
  • The selection controller trims text for its floating action payload but does not change the browser's live Range:
    function readSelectionWithinNode(
    node: HTMLElement | null,
    anchor: SelectionAnchor | null,
    ): MessageProseSelection | null {
    if (node === null || typeof window === "undefined") return null;
    const selection = window.getSelection();
    if (selection === null || selection.rangeCount === 0) return null;
    const range = selection.getRangeAt(0);
    const accepted = isSelectionWithinNode(node, {
    isCollapsed: selection.isCollapsed,
    anchorNode: selection.anchorNode,
    focusNode: selection.focusNode,
    commonAncestorContainer: range.commonAncestorContainer,
    });
    if (accepted) {
    const text = selection.toString().trim();
    const rect = firstClientRect(range);
    return toMessageProseSelection({ anchor, rect, text });
    }
    const text = selection.toString().trim();
    if (isSelectionBoundarySpillWithinNode(node, range, text)) {
    const rect = firstClientRect(range);
    return toMessageProseSelection({ anchor, rect, text });
    }
    return null;
    }
  • Normal composer paste preserves the plain-text newlines supplied by the clipboard:
    const plainText = event.clipboardData?.getData("text/plain") ?? "";
    const sliceHasBlockquote = promptEditorSliceHasBlockquote(slice);
    if (sliceHasBlockquote || plainTextHasQuoteLine(plainText)) {
    event.preventDefault();
    const pastedValue = trimTrailingPromptNewlines(
    sliceHasBlockquote
    ? promptEditorValueFromSlice(slice, view.state.schema)
    : promptEditorValueFromPlainText(plainText, promptActions),
    );
    if (pastedValue.text.length === 0) return true;
    const currentEditor = editorRef.current;
    const pastedContent =
    promptEditorContentFromValue(pastedValue, {
    richTextMarkdown: richTextEditing,
    }).content ?? [];
    currentEditor
    ?.chain()
    .focus()
    .insertContent(pastedContent)
    .setMeta("uiEvent", "paste")
    .run();
    if (currentEditor && !currentEditor.isDestroyed) {
    const nextValue = trimTrailingPromptNewlines(
    promptEditorValueFromDoc(currentEditor.state.doc),
    );
    lastSyncedEditorValueRef.current = nextValue;
    onChangeRef.current(nextValue.text, nextValue.mentions);
    }
    return true;
    }
    const pastedValue = promptEditorValueFromClipboardPaste(
    event.clipboardData ?? null,
    promptActions,
    );
    if (pastedValue === null) return false;
    event.preventDefault();
    if (pastedValue.text.length === 0) return true;
    editorRef.current
    ?.chain()
    .focus()
    .insertContent(promptEditorInlineContentFromValue(pastedValue))
    .setMeta("uiEvent", "paste")
    .run();
    return true;

Observed selection state during reproduction:

Visible selection: one assistant sentence
Native selection / clipboard suffix: \n\n\n\n\n\n\n
Anchor: assistant paragraph
Focus: a following timeline row

What you ruled out

  • Still reproduces on main at 31a190ddb5f0ffb86cb8dcd80482497663de2685 with a clean development data directory.
  • Does not reproduce when both native Range endpoints remain inside the assistant prose wrapper.
  • Does not depend on the provider after the assistant message has rendered.
  • Pressing Enter strips the trailing newlines before submission; the problem is limited to the draft's visual state.
  • Searches for copy paste newlines text selection, clipboard selection, and newlines paste chat found no matching open or closed issue. Clipboard copy actions fail on plain-HTTP LAN origins #1590 concerns clipboard permission failures on plain-HTTP origins, not extra newlines from selection serialization.

Suggested priority and effort

Low priority: this is a visual-quality issue in the draft composer. It does not change the submitted message, lose data, or block the workflow.

Checks

  • Reproduced on main.
  • Searched open and closed issues for duplicates.
  • Included exact environment, steps, observed clipboard contents, and commit permalinks.

AGENT GENERATED

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-reproBug reproduced independently; see linked reportuiApp shell, sidebar, composer, rendering

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions