mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The inline markdown editor (MarkdownEditor / MDXEditor) is used to edit agent instructions, issue descriptions, and other content > - When users paste agent instructions copied from terminals or consoles, extra leading whitespace is uniformly added to every line > - PR #2572 fixed markdown structure preservation on paste but did not address the leading whitespace (dedent) problem > - This pull request adds a Lexical paste normalization plugin that strips common leading whitespace and normalizes line endings before MDXEditor processes pasted content > - The benefit is that pasted content from terminals/consoles renders correctly without manual cleanup ## What Changed - **`ui/src/lib/normalize-markdown.ts`** — Pure utility that computes minimum common indentation across non-empty lines and strips it (dedent), plus CRLF → LF normalization - **`ui/src/lib/paste-normalization.ts`** — Lexical `PASTE_COMMAND` plugin at `CRITICAL` priority that intercepts plain-text pastes, normalizes the markdown, and re-dispatches cleaned content for MDXEditor to process. Skips HTML-rich pastes. - **`ui/src/components/MarkdownEditor.tsx`** — Registers the new plugin; updates PR #2572's `handlePasteCapture` to use `normalizeMarkdown()` (dedent + CRLF) instead of `normalizePastedMarkdown()` (CRLF only) for the markdown-routing path - **`ui/src/lib/paste-normalization.test.ts`** — 9 unit tests covering dedent, CRLF normalization, mixed indent, empty lines, single-line passthrough, and edge cases ## Verification - `pnpm --dir ui exec vitest run src/lib/paste-normalization.test.ts` — 9 tests pass - Manual: paste indented agent instructions from a terminal into any inline markdown editor and confirm leading whitespace is stripped ## Risks - Low risk. The plugin only activates for plain-text pastes (no HTML clipboard data). HTML/rich pastes pass through unchanged. Single-line pastes are not modified. The dedent logic is conservative — it only strips whitespace common to all non-empty lines. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
import { createRootEditorSubscription$, realmPlugin } from "@mdxeditor/editor";
|
|
import { COMMAND_PRIORITY_CRITICAL, PASTE_COMMAND } from "lexical";
|
|
import { looksLikeMarkdownPaste } from "./markdownPaste";
|
|
import { normalizeMarkdown } from "./normalize-markdown";
|
|
|
|
/**
|
|
* MDXEditor/Lexical plugin that intercepts paste events and normalizes
|
|
* markdown content before the editor processes it. Fixes issues with
|
|
* extra leading spaces when pasting from terminals or consoles.
|
|
*/
|
|
export const pasteNormalizationPlugin = realmPlugin({
|
|
init(realm) {
|
|
realm.pub(createRootEditorSubscription$, [
|
|
(editor) => {
|
|
let skipNext = false;
|
|
|
|
return editor.registerCommand(
|
|
PASTE_COMMAND,
|
|
(event) => {
|
|
if (skipNext) {
|
|
skipNext = false;
|
|
return false;
|
|
}
|
|
|
|
const clipboardData =
|
|
event instanceof ClipboardEvent ? event.clipboardData : null;
|
|
if (!clipboardData) return false;
|
|
|
|
const text = clipboardData.getData("text/plain");
|
|
if (!text) return false;
|
|
|
|
// If there's HTML content, the source app already formatted it —
|
|
// let the default paste handler deal with rich content as-is.
|
|
if (clipboardData.getData("text/html")) return false;
|
|
|
|
// Markdown-looking pastes are handled by MarkdownEditor.tsx via
|
|
// insertMarkdown(), so the plugin only owns the plain-text fallback.
|
|
if (looksLikeMarkdownPaste(text)) return false;
|
|
|
|
const cleaned = normalizeMarkdown(text);
|
|
if (cleaned === text) return false;
|
|
|
|
// Prevent the original paste from being processed
|
|
if (event instanceof ClipboardEvent) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
// Re-dispatch with cleaned data so MDXEditor's handler processes it
|
|
const dt = new DataTransfer();
|
|
dt.setData("text/plain", cleaned);
|
|
const newEvent = new ClipboardEvent("paste", {
|
|
clipboardData: dt,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
});
|
|
|
|
skipNext = true;
|
|
editor.dispatchCommand(PASTE_COMMAND, newEvent);
|
|
return true;
|
|
},
|
|
COMMAND_PRIORITY_CRITICAL,
|
|
);
|
|
},
|
|
]);
|
|
},
|
|
});
|