[research] Composer: detect pasted code and offer a fenced code block #107

Closed
opened 2026-09-17 01:39:34 -04:00 by jared · 2 comments
Owner

When a multi-line paste looks like code (consistent indentation, braces/semicolons, keywords, or the clipboard carries text/html from an editor with monospace styling), offer to wrap it in a fenced code block with a language guess instead of sending a wall of text.

Research first:

  1. How the Slate editor handles paste today (src/app/features/room/RoomInput.tsx handlePaste, src/app/components/editor/input.ts) and how code blocks are represented (BlockType.CodeBlock) and serialised to formatted_body.
  2. Heuristic design and false-positive control (plain prose with line breaks must NOT be converted); an unobtrusive prompt ("Paste as code?" inline chip) vs. automatic conversion with undo.
  3. Language guessing options that are small (no highlight.js auto-detect payload if avoidable — Prism is already a dep).
    Deliverable: findings + a plan in this issue.
When a multi-line paste looks like code (consistent indentation, braces/semicolons, keywords, or the clipboard carries `text/html` from an editor with monospace styling), offer to wrap it in a fenced code block with a language guess instead of sending a wall of text. **Research first:** 1. How the Slate editor handles paste today (`src/app/features/room/RoomInput.tsx` `handlePaste`, `src/app/components/editor/input.ts`) and how code blocks are represented (`BlockType.CodeBlock`) and serialised to `formatted_body`. 2. Heuristic design and false-positive control (plain prose with line breaks must NOT be converted); an unobtrusive prompt ("Paste as code?" inline chip) vs. automatic conversion with undo. 3. Language guessing options that are small (no highlight.js auto-detect payload if avoidable — Prism is already a dep). Deliverable: findings + a plan in this issue.
jared added this to the Features 2026-Q4 milestone 2026-09-17 01:39:34 -04:00
jared added the enhancementpriority: lowuxarea: messaging labels 2026-09-17 01:39:34 -04:00
jared self-assigned this 2026-09-17 01:39:34 -04:00
Author
Owner

Findings (2026-09-20)

1. Paste today. RoomInput.tsx:406-423 handlePaste: first the file paste hook (useFilePasteHandler.ts — images/files → upload board), then, only when tracking-param stripping is on, it rewrites text/plain and re-inserts via ReactEditor.insertData. Everything else is Slate's default insertData: text/plain is split on newlines into paragraphs (one BlockType.Paragraph per line); text/html from editors is not consumed (Slate only uses application/x-slate-fragment or plain text), so VS Code's monospace HTML is discarded — we can still read it from clipboardData as a signal. Code blocks are BlockType.CodeBlock containing BlockType.CodeLine children (editor/types.ts:13-14), serialised to <pre><code>…</code></pre> with one line per CodeLine (output.ts:86-90) and to the plain body as fenced text; toggleBlock(editor, BlockType.CodeBlock) (used by the toolbar, Toolbar.tsx:101) converts the selected paragraphs in place. No language class is emitted today (<code class="language-x">), so a language guess would be a small addition to output.ts.

2. Heuristic (pure, unit-testable — utils/looksLikeCode.ts). Score a paste only when it has ≥ 3 non-empty lines; require ≥ 2 of:

  • ≥ 40 % of lines start with 2+ spaces or a tab (indentation), and indentation depth changes ≥ 2 times;
  • ≥ 30 % of lines end with ;, {, }, ), :, or , (statement terminators);
  • ≥ 2 lines contain =/=>/->/::/() and at least one of {,},[,];
  • keyword hits: ≥ 2 distinct from a small list (function, const, let, var, return, import, export, class, def, fn, pub, struct, if (, for (, while (, #include, SELECT, FROM, <?php, </), case-sensitive except SQL;
  • clipboard text/html contains font-family: with a monospace face, or a <pre/<code.
    Hard no (prose guard): average line length > 90 chars with sentence punctuation (. twice per line), or > 60 % of lines are plain words with spaces and no symbols, or the text is a URL list / quoted reply (> ). Language guess only when confident ({+;+const|let|=> → js/ts; def +: → python; #include/-> → c; pub fn → rust; SELECT → sql), else none.

3. UX recommendation: prompt, never auto-convert. After a qualifying paste, show an inline chip above the composer — "Paste as code block? · js · Yes · No" — for 8 s or until the next keystroke; Yes runs toggleBlock(editor, CodeBlock) over the just-inserted range (we know the selection before/after insert) and remembers the answer per session; No dismisses; typing dismisses. Ctrl/⌘-Shift-V (paste as code) as the explicit path. Auto-conversion is out: the prose guard cannot be perfect, and silently changing a message's shape erodes trust more than a dismissible chip costs. A setting "Offer to paste as code" (default on) lets people turn the chip off.

4. Cost. ~120 lines heuristic + tests with fixtures (JS, Python, SQL, Rust, a log excerpt, a stack trace, prose with line breaks, a markdown list, a quoted reply), ~80 lines chip + toggle wiring, +10 in output.ts for class="language-x". Waiting for a yes on the prompt-vs-auto call before building.

## Findings (2026-09-20) **1. Paste today.** `RoomInput.tsx:406-423` `handlePaste`: first the file paste hook (`useFilePasteHandler.ts` — images/files → upload board), then, only when tracking-param stripping is on, it rewrites `text/plain` and re-inserts via `ReactEditor.insertData`. Everything else is Slate's default `insertData`: `text/plain` is split on newlines into **paragraphs** (one `BlockType.Paragraph` per line); `text/html` from editors is *not* consumed (Slate only uses `application/x-slate-fragment` or plain text), so VS Code's monospace HTML is discarded — we can still *read* it from `clipboardData` as a signal. Code blocks are `BlockType.CodeBlock` containing `BlockType.CodeLine` children (`editor/types.ts:13-14`), serialised to `<pre><code>…</code></pre>` with one line per CodeLine (`output.ts:86-90`) and to the plain `body` as fenced text; `toggleBlock(editor, BlockType.CodeBlock)` (used by the toolbar, `Toolbar.tsx:101`) converts the selected paragraphs in place. No language class is emitted today (`<code class="language-x">`), so a language guess would be a small addition to `output.ts`. **2. Heuristic (pure, unit-testable — `utils/looksLikeCode.ts`).** Score a paste only when it has ≥ 3 non-empty lines; require ≥ 2 of: - ≥ 40 % of lines start with 2+ spaces or a tab (indentation), and indentation depth changes ≥ 2 times; - ≥ 30 % of lines end with `;`, `{`, `}`, `)`, `:`, or `,` (statement terminators); - ≥ 2 lines contain `=`/`=>`/`->`/`::`/`()` and at least one of `{`,`}`,`[`,`]`; - keyword hits: ≥ 2 distinct from a small list (`function`, `const`, `let`, `var`, `return`, `import`, `export`, `class`, `def`, `fn`, `pub`, `struct`, `if (`, `for (`, `while (`, `#include`, `SELECT`, `FROM`, `<?php`, `</`), case-sensitive except SQL; - clipboard `text/html` contains `font-family:` with a monospace face, or a `<pre`/`<code`. Hard **no** (prose guard): average line length > 90 chars with sentence punctuation (`. ` twice per line), or > 60 % of lines are plain words with spaces and no symbols, or the text is a URL list / quoted reply (`> `). Language guess only when confident (`{`+`;`+`const|let|=>` → js/ts; `def `+`:` → python; `#include`/`->` → c; `pub fn` → rust; `SELECT` → sql), else none. **3. UX recommendation: prompt, never auto-convert.** After a qualifying paste, show an inline chip above the composer — "Paste as code block? · `js` · **Yes** · No" — for 8 s or until the next keystroke; Yes runs `toggleBlock(editor, CodeBlock)` over the just-inserted range (we know the selection before/after insert) and remembers the answer per session; No dismisses; typing dismisses. Ctrl/⌘-Shift-V (paste as code) as the explicit path. Auto-conversion is out: the prose guard cannot be perfect, and silently changing a message's shape erodes trust more than a dismissible chip costs. A setting "Offer to paste as code" (default on) lets people turn the chip off. **4. Cost.** ~120 lines heuristic + tests with fixtures (JS, Python, SQL, Rust, a log excerpt, a stack trace, prose with line breaks, a markdown list, a quoted reply), ~80 lines chip + toggle wiring, +10 in `output.ts` for `class="language-x"`. Waiting for a yes on the prompt-vs-auto call before building.
jared added the needs-human-review label 2026-09-20 15:12:17 -04:00
Author
Owner

Built as planned (75d55e86): a prompt, never an automatic conversion

  • Chip above the composer after a paste that looks like code: "That looks like code. Format it as a code block (js)?" with Format as code and No thanks. Typing, No, or 8 s dismiss it. Tab and arrow keys don't, so a keyboard user can reach the buttons.
  • Setting: Settings → Editor → Offer to Format Pasted Code, on by default.
  • Detector (utils/looksLikeCode.ts, pure) follows the heuristic above:
    • It needs at least 3 non-empty lines and at least 2 signals.
    • The signals are indentation with depth changes, statement terminators, operators with brackets, keywords, monospace or <pre> in the clipboard HTML, and SQL clause lines. The SQL signal was added because SQL has almost no symbols.
    • The prose guards (quoted replies, URL lists, long sentence-punctuated lines, plain-word lines, markdown lists) veto a paste outright.
    • A language is guessed only when fairly sure: js, ts, python, sql, rust, c or php.
  • Accepting rebuilds the pasted paragraphs as one code block from their plain text. Code lines hold text only, and a paste can produce links. The caret ends up after the block.
  • Code blocks carry an optional language, rendered as <code class="language-js">. Only a plain identifier is accepted; anything else is dropped.

Verified in Chromium:

  • Pasting a JS function shows the chip.
  • After Format as code, the sent formatted_body is <pre><code class="language-js">… with the indentation intact.
  • A three-line prose paste shows no chip.
  • Typing after a paste dismisses the chip.

Tests: 14 detector fixtures (JS, TS, Python, SQL, Rust, JSON, editor HTML; prose, a long paragraph, a list, a quoted reply, links, a log) and 2 output tests. The chromium e2e suite passes (19).

chip

## Built as planned (`75d55e86`): a prompt, never an automatic conversion - **Chip above the composer** after a paste that looks like code: *"That looks like code. Format it as a code block (js)?"* with **Format as code** and **No thanks**. Typing, No, or 8 s dismiss it. Tab and arrow keys don't, so a keyboard user can reach the buttons. - **Setting:** Settings → Editor → *Offer to Format Pasted Code*, on by default. - **Detector** (`utils/looksLikeCode.ts`, pure) follows the heuristic above: - It needs at least 3 non-empty lines and at least 2 signals. - The signals are indentation with depth changes, statement terminators, operators with brackets, keywords, monospace or `<pre>` in the clipboard HTML, and SQL clause lines. The SQL signal was added because SQL has almost no symbols. - The prose guards (quoted replies, URL lists, long sentence-punctuated lines, plain-word lines, markdown lists) veto a paste outright. - A language is guessed only when fairly sure: js, ts, python, sql, rust, c or php. - **Accepting** rebuilds the pasted paragraphs as one code block from their plain text. Code lines hold text only, and a paste can produce links. The caret ends up after the block. - **Code blocks carry an optional language**, rendered as `<code class="language-js">`. Only a plain identifier is accepted; anything else is dropped. **Verified** in Chromium: - Pasting a JS function shows the chip. - After *Format as code*, the sent `formatted_body` is `<pre><code class="language-js">…` with the indentation intact. - A three-line prose paste shows no chip. - Typing after a paste dismisses the chip. **Tests:** 14 detector fixtures (JS, TS, Python, SQL, Rust, JSON, editor HTML; prose, a long paragraph, a list, a quoted reply, links, a log) and 2 output tests. The chromium e2e suite passes (19). ![chip](https://code.lotusguild.org/attachments/3524990d-df38-47dd-b736-81394666860d)
jared closed this issue 2026-09-25 11:06:14 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#107