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

Open
opened 2026-09-17 01:39:34 -04:00 by jared · 1 comment
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
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#107