2026-02-20 13:35:15 -06:00
|
|
|
import {
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
type ClipboardEvent,
|
2026-02-20 13:35:15 -06:00
|
|
|
forwardRef,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useImperativeHandle,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
type DragEvent,
|
|
|
|
|
} from "react";
|
2026-03-26 07:22:24 -05:00
|
|
|
import { createPortal } from "react-dom";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import {
|
2026-03-02 16:44:03 -06:00
|
|
|
CodeMirrorEditor,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
MDXEditor,
|
2026-02-26 16:33:48 -06:00
|
|
|
codeBlockPlugin,
|
|
|
|
|
codeMirrorPlugin,
|
2026-03-02 16:44:03 -06:00
|
|
|
type CodeBlockEditorDescriptor,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
type MDXEditorMethods,
|
|
|
|
|
headingsPlugin,
|
|
|
|
|
imagePlugin,
|
|
|
|
|
linkDialogPlugin,
|
|
|
|
|
linkPlugin,
|
|
|
|
|
listsPlugin,
|
|
|
|
|
markdownShortcutPlugin,
|
|
|
|
|
quotePlugin,
|
2026-02-26 16:33:48 -06:00
|
|
|
tablePlugin,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
thematicBreakPlugin,
|
|
|
|
|
type RealmPlugin,
|
|
|
|
|
} from "@mdxeditor/editor";
|
2026-03-21 14:48:10 -05:00
|
|
|
import { buildAgentMentionHref, buildProjectMentionHref } from "@paperclipai/shared";
|
2026-04-04 17:00:40 -05:00
|
|
|
import { Boxes } from "lucide-react";
|
2026-03-21 14:48:10 -05:00
|
|
|
import { AgentIcon } from "./AgentIconPicker";
|
|
|
|
|
import { applyMentionChipDecoration, clearMentionChipDecoration, parseMentionChipHref } from "../lib/mention-chips";
|
2026-03-23 20:41:50 -05:00
|
|
|
import { MentionAwareLinkNode, mentionAwareLinkNodeReplacement } from "../lib/mention-aware-link-node";
|
2026-03-22 06:34:15 -05:00
|
|
|
import { mentionDeletionPlugin } from "../lib/mention-deletion";
|
fix: autoformat pasted markdown in inline editor (#2673)
## 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>
2026-04-04 11:21:27 -07:00
|
|
|
import { looksLikeMarkdownPaste } from "../lib/markdownPaste";
|
|
|
|
|
import { normalizeMarkdown } from "../lib/normalize-markdown";
|
|
|
|
|
import { pasteNormalizationPlugin } from "../lib/paste-normalization";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { cn } from "../lib/utils";
|
2026-04-04 17:00:40 -05:00
|
|
|
import { useEditorAutocomplete, type SkillCommandOption } from "../context/EditorAutocompleteContext";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
/* ---- Mention types ---- */
|
|
|
|
|
|
|
|
|
|
export interface MentionOption {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
2026-03-02 13:31:58 -06:00
|
|
|
kind?: "agent" | "project";
|
2026-03-21 14:48:10 -05:00
|
|
|
agentId?: string;
|
|
|
|
|
agentIcon?: string | null;
|
2026-03-02 13:31:58 -06:00
|
|
|
projectId?: string;
|
|
|
|
|
projectColor?: string | null;
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Editor props ---- */
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
interface MarkdownEditorProps {
|
|
|
|
|
value: string;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
className?: string;
|
|
|
|
|
contentClassName?: string;
|
|
|
|
|
onBlur?: () => void;
|
|
|
|
|
imageUploadHandler?: (file: File) => Promise<string>;
|
2026-04-05 06:39:20 -05:00
|
|
|
/** Called when a non-image file is dropped onto the editor (e.g. .zip). */
|
|
|
|
|
onDropFile?: (file: File) => Promise<void>;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
bordered?: boolean;
|
2026-03-02 13:31:58 -06:00
|
|
|
/** List of mentionable entities. Enables @-mention autocomplete. */
|
2026-02-20 13:35:15 -06:00
|
|
|
mentions?: MentionOption[];
|
|
|
|
|
/** Called on Cmd/Ctrl+Enter */
|
|
|
|
|
onSubmit?: () => void;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MarkdownEditorRef {
|
|
|
|
|
focus: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 09:21:44 -05:00
|
|
|
function escapeRegExp(value: string): string {
|
|
|
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:59:20 -05:00
|
|
|
function isSafeMarkdownLinkUrl(url: string): boolean {
|
|
|
|
|
const trimmed = url.trim();
|
|
|
|
|
if (!trimmed) return true;
|
|
|
|
|
return !/^(javascript|data|vbscript):/i.test(trimmed);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
/* ---- Mention detection helpers ---- */
|
|
|
|
|
|
|
|
|
|
interface MentionState {
|
2026-04-04 17:00:40 -05:00
|
|
|
trigger: "mention" | "skill";
|
|
|
|
|
marker: "@" | "/";
|
2026-02-20 13:35:15 -06:00
|
|
|
query: string;
|
|
|
|
|
top: number;
|
|
|
|
|
left: number;
|
2026-03-26 07:22:24 -05:00
|
|
|
/** Viewport-relative coords for portal positioning */
|
|
|
|
|
viewportTop: number;
|
|
|
|
|
viewportLeft: number;
|
2026-02-20 13:35:15 -06:00
|
|
|
textNode: Text;
|
|
|
|
|
atPos: number;
|
|
|
|
|
endPos: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 17:00:40 -05:00
|
|
|
type AutocompleteOption = MentionOption | SkillCommandOption;
|
|
|
|
|
|
2026-04-04 13:12:06 -05:00
|
|
|
interface MentionMenuViewport {
|
|
|
|
|
offsetLeft: number;
|
|
|
|
|
offsetTop: number;
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 19:10:58 -05:00
|
|
|
interface MentionMenuSize {
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:12:06 -05:00
|
|
|
const MENTION_MENU_WIDTH = 188;
|
|
|
|
|
const MENTION_MENU_HEIGHT = 208;
|
|
|
|
|
const MENTION_MENU_PADDING = 8;
|
2026-04-04 19:10:58 -05:00
|
|
|
const MENTION_MENU_ROW_HEIGHT = 34;
|
|
|
|
|
const MENTION_MENU_CHROME_HEIGHT = 8;
|
2026-04-04 13:12:06 -05:00
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const CODE_BLOCK_LANGUAGES: Record<string, string> = {
|
|
|
|
|
txt: "Text",
|
|
|
|
|
md: "Markdown",
|
|
|
|
|
js: "JavaScript",
|
|
|
|
|
jsx: "JavaScript (JSX)",
|
|
|
|
|
ts: "TypeScript",
|
|
|
|
|
tsx: "TypeScript (TSX)",
|
|
|
|
|
json: "JSON",
|
|
|
|
|
bash: "Bash",
|
|
|
|
|
sh: "Shell",
|
|
|
|
|
python: "Python",
|
|
|
|
|
go: "Go",
|
|
|
|
|
rust: "Rust",
|
|
|
|
|
sql: "SQL",
|
|
|
|
|
html: "HTML",
|
|
|
|
|
css: "CSS",
|
|
|
|
|
yaml: "YAML",
|
|
|
|
|
yml: "YAML",
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 16:44:03 -06:00
|
|
|
const FALLBACK_CODE_BLOCK_DESCRIPTOR: CodeBlockEditorDescriptor = {
|
|
|
|
|
// Keep this lower than codeMirrorPlugin's descriptor priority so known languages
|
|
|
|
|
// still use the standard matching path; this catches malformed/unknown fences.
|
|
|
|
|
priority: 0,
|
|
|
|
|
match: () => true,
|
|
|
|
|
Editor: CodeMirrorEditor,
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-04 19:10:58 -05:00
|
|
|
export function findMentionMatch(
|
|
|
|
|
text: string,
|
|
|
|
|
offset: number,
|
|
|
|
|
): Pick<MentionState, "trigger" | "marker" | "query" | "atPos" | "endPos"> | null {
|
2026-02-20 13:35:15 -06:00
|
|
|
let atPos = -1;
|
2026-04-04 17:00:40 -05:00
|
|
|
let trigger: MentionState["trigger"] | null = null;
|
|
|
|
|
let marker: MentionState["marker"] | null = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
for (let i = offset - 1; i >= 0; i--) {
|
|
|
|
|
const ch = text[i];
|
2026-04-04 17:00:40 -05:00
|
|
|
if (ch === "@" || ch === "/") {
|
2026-02-20 13:35:15 -06:00
|
|
|
if (i === 0 || /\s/.test(text[i - 1])) {
|
|
|
|
|
atPos = i;
|
2026-04-04 17:00:40 -05:00
|
|
|
trigger = ch === "@" ? "mention" : "skill";
|
|
|
|
|
marker = ch;
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-04-04 19:10:58 -05:00
|
|
|
if (ch === "\n" || ch === "\r") break;
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (atPos === -1) return null;
|
|
|
|
|
const query = text.slice(atPos + 1, offset);
|
2026-04-04 19:10:58 -05:00
|
|
|
if (trigger === "skill" && /\s/.test(query)) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
trigger: trigger ?? "mention",
|
|
|
|
|
marker: marker ?? "@",
|
|
|
|
|
query,
|
|
|
|
|
atPos,
|
|
|
|
|
endPos: offset,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function detectMention(container: HTMLElement): MentionState | null {
|
|
|
|
|
const sel = window.getSelection();
|
|
|
|
|
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
|
|
|
|
|
|
|
|
|
|
const range = sel.getRangeAt(0);
|
|
|
|
|
const textNode = range.startContainer;
|
|
|
|
|
if (textNode.nodeType !== Node.TEXT_NODE) return null;
|
|
|
|
|
if (!container.contains(textNode)) return null;
|
|
|
|
|
|
|
|
|
|
const text = textNode.textContent ?? "";
|
|
|
|
|
const offset = range.startOffset;
|
|
|
|
|
const match = findMentionMatch(text, offset);
|
|
|
|
|
if (!match) return null;
|
2026-02-20 13:35:15 -06:00
|
|
|
|
|
|
|
|
// Get position relative to container
|
|
|
|
|
const tempRange = document.createRange();
|
2026-04-04 19:10:58 -05:00
|
|
|
tempRange.setStart(textNode, match.atPos);
|
|
|
|
|
tempRange.setEnd(textNode, match.atPos + 1);
|
2026-02-20 13:35:15 -06:00
|
|
|
const rect = tempRange.getBoundingClientRect();
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-04 19:10:58 -05:00
|
|
|
trigger: match.trigger,
|
|
|
|
|
marker: match.marker,
|
|
|
|
|
query: match.query,
|
2026-02-20 13:35:15 -06:00
|
|
|
top: rect.bottom - containerRect.top,
|
|
|
|
|
left: rect.left - containerRect.left,
|
2026-03-26 07:22:24 -05:00
|
|
|
viewportTop: rect.bottom,
|
|
|
|
|
viewportLeft: rect.left,
|
2026-02-20 13:35:15 -06:00
|
|
|
textNode: textNode as Text,
|
2026-04-04 19:10:58 -05:00
|
|
|
atPos: match.atPos,
|
|
|
|
|
endPos: match.endPos,
|
2026-02-20 13:35:15 -06:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:12:06 -05:00
|
|
|
function getMentionMenuViewport(): MentionMenuViewport {
|
|
|
|
|
const viewport = window.visualViewport;
|
|
|
|
|
if (viewport) {
|
|
|
|
|
return {
|
|
|
|
|
offsetLeft: viewport.offsetLeft,
|
|
|
|
|
offsetTop: viewport.offsetTop,
|
|
|
|
|
width: viewport.width,
|
|
|
|
|
height: viewport.height,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
offsetLeft: 0,
|
|
|
|
|
offsetTop: 0,
|
|
|
|
|
width: window.innerWidth,
|
|
|
|
|
height: window.innerHeight,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function computeMentionMenuPosition(
|
|
|
|
|
anchor: Pick<MentionState, "viewportTop" | "viewportLeft">,
|
|
|
|
|
viewport: MentionMenuViewport,
|
2026-04-04 19:10:58 -05:00
|
|
|
menuSize: MentionMenuSize = { width: MENTION_MENU_WIDTH, height: MENTION_MENU_HEIGHT },
|
2026-04-04 13:12:06 -05:00
|
|
|
) {
|
|
|
|
|
const minLeft = viewport.offsetLeft + MENTION_MENU_PADDING;
|
2026-04-04 19:10:58 -05:00
|
|
|
const maxLeft = viewport.offsetLeft + viewport.width - menuSize.width;
|
2026-04-04 13:12:06 -05:00
|
|
|
const minTop = viewport.offsetTop + MENTION_MENU_PADDING;
|
2026-04-04 19:10:58 -05:00
|
|
|
const maxTop = viewport.offsetTop + viewport.height - menuSize.height;
|
2026-04-04 13:12:06 -05:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
top: Math.max(minTop, Math.min(viewport.offsetTop + anchor.viewportTop + 4, maxTop)),
|
|
|
|
|
left: Math.max(minLeft, Math.min(viewport.offsetLeft + anchor.viewportLeft, maxLeft)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 19:10:58 -05:00
|
|
|
function getMentionMenuSize(optionCount: number): MentionMenuSize {
|
|
|
|
|
const visibleRows = Math.max(1, Math.min(optionCount, 8));
|
|
|
|
|
return {
|
|
|
|
|
width: MENTION_MENU_WIDTH,
|
|
|
|
|
height: Math.min(
|
|
|
|
|
MENTION_MENU_HEIGHT,
|
|
|
|
|
visibleRows * MENTION_MENU_ROW_HEIGHT + MENTION_MENU_CHROME_HEIGHT,
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
function nodeInsideCodeLike(container: HTMLElement, node: Node | null): boolean {
|
|
|
|
|
if (!node || !container.contains(node)) return false;
|
|
|
|
|
const el = node.nodeType === Node.ELEMENT_NODE
|
|
|
|
|
? (node as HTMLElement)
|
|
|
|
|
: node.parentElement;
|
|
|
|
|
return Boolean(el?.closest("pre, code"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSelectionInsideCodeLikeElement(container: HTMLElement | null) {
|
|
|
|
|
if (!container) return false;
|
|
|
|
|
const selection = window.getSelection();
|
|
|
|
|
if (!selection) return false;
|
|
|
|
|
for (const node of [selection.anchorNode, selection.focusNode]) {
|
|
|
|
|
if (nodeInsideCodeLike(container, node)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
function mentionMarkdown(option: MentionOption): string {
|
|
|
|
|
if (option.kind === "project" && option.projectId) {
|
|
|
|
|
return `[@${option.name}](${buildProjectMentionHref(option.projectId, option.projectColor ?? null)}) `;
|
|
|
|
|
}
|
2026-03-21 14:48:10 -05:00
|
|
|
const agentId = option.agentId ?? option.id.replace(/^agent:/, "");
|
|
|
|
|
return `[@${option.name}](${buildAgentMentionHref(agentId, option.agentIcon ?? null)}) `;
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 17:00:40 -05:00
|
|
|
function skillMarkdown(option: SkillCommandOption): string {
|
|
|
|
|
return `[/${option.slug}](${option.href}) `;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function autocompleteMarkdown(option: AutocompleteOption): string {
|
|
|
|
|
return option.kind === "skill" ? skillMarkdown(option) : mentionMarkdown(option);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Replace the active autocomplete token in the markdown string with the selected token. */
|
|
|
|
|
function applyMention(markdown: string, state: MentionState, option: AutocompleteOption): string {
|
|
|
|
|
const search = `${state.marker}${state.query}`;
|
|
|
|
|
const replacement = autocompleteMarkdown(option);
|
2026-02-20 14:53:46 -06:00
|
|
|
const idx = markdown.lastIndexOf(search);
|
|
|
|
|
if (idx === -1) return markdown;
|
|
|
|
|
return markdown.slice(0, idx) + replacement + markdown.slice(idx + search.length);
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Component ---- */
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(function MarkdownEditor({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
placeholder,
|
|
|
|
|
className,
|
|
|
|
|
contentClassName,
|
|
|
|
|
onBlur,
|
|
|
|
|
imageUploadHandler,
|
2026-04-05 06:39:20 -05:00
|
|
|
onDropFile,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
bordered = true,
|
2026-02-20 13:35:15 -06:00
|
|
|
mentions,
|
|
|
|
|
onSubmit,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}: MarkdownEditorProps, forwardedRef) {
|
2026-04-04 17:00:40 -05:00
|
|
|
const { slashCommands } = useEditorAutocomplete();
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
const ref = useRef<MDXEditorMethods>(null);
|
|
|
|
|
const valueRef = useRef(value);
|
|
|
|
|
valueRef.current = value;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const latestValueRef = useRef(value);
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
const initialChildOnChangeRef = useRef(true);
|
|
|
|
|
/**
|
|
|
|
|
* After imperative `setMarkdown` (prop sync, mentions, image upload), MDXEditor may emit `onChange`
|
|
|
|
|
* with the same markdown. Skip notifying the parent for that echo so controlled parents that
|
|
|
|
|
* normalize or transform values cannot loop. Replaces the older blur/focus gate for the same concern.
|
|
|
|
|
*/
|
|
|
|
|
const echoIgnoreMarkdownRef = useRef<string | null>(null);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
|
|
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
|
|
|
const dragDepthRef = useRef(0);
|
|
|
|
|
|
2026-03-03 09:36:49 -06:00
|
|
|
// Stable ref for imageUploadHandler so plugins don't recreate on every render
|
|
|
|
|
const imageUploadHandlerRef = useRef(imageUploadHandler);
|
|
|
|
|
imageUploadHandlerRef.current = imageUploadHandler;
|
|
|
|
|
|
2026-02-20 14:59:20 -06:00
|
|
|
// Mention state (ref kept in sync so callbacks always see the latest value)
|
2026-02-20 13:35:15 -06:00
|
|
|
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
2026-02-20 14:59:20 -06:00
|
|
|
const mentionStateRef = useRef<MentionState | null>(null);
|
2026-02-20 13:35:15 -06:00
|
|
|
const [mentionIndex, setMentionIndex] = useState(0);
|
2026-04-04 17:00:40 -05:00
|
|
|
const mentionActive = mentionState !== null && (
|
|
|
|
|
(mentionState.trigger === "mention" && Boolean(mentions?.length))
|
|
|
|
|
|| (mentionState.trigger === "skill" && slashCommands.length > 0)
|
|
|
|
|
);
|
2026-03-21 14:48:10 -05:00
|
|
|
const mentionOptionByKey = useMemo(() => {
|
|
|
|
|
const map = new Map<string, MentionOption>();
|
2026-03-02 13:31:58 -06:00
|
|
|
for (const mention of mentions ?? []) {
|
2026-03-21 14:48:10 -05:00
|
|
|
if (mention.kind === "agent") {
|
|
|
|
|
const agentId = mention.agentId ?? mention.id.replace(/^agent:/, "");
|
|
|
|
|
map.set(`agent:${agentId}`, mention);
|
|
|
|
|
}
|
2026-03-02 13:31:58 -06:00
|
|
|
if (mention.kind === "project" && mention.projectId) {
|
2026-03-21 14:48:10 -05:00
|
|
|
map.set(`project:${mention.projectId}`, mention);
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}, [mentions]);
|
2026-02-20 13:35:15 -06:00
|
|
|
|
2026-04-09 06:12:43 -05:00
|
|
|
const setEditorRef = useCallback((instance: MDXEditorMethods | null) => {
|
|
|
|
|
ref.current = instance;
|
|
|
|
|
if (!instance) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (valueRef.current !== latestValueRef.current) {
|
|
|
|
|
// Re-apply the latest controlled value once MDXEditor exposes its imperative API.
|
|
|
|
|
echoIgnoreMarkdownRef.current = valueRef.current;
|
|
|
|
|
instance.setMarkdown(valueRef.current);
|
|
|
|
|
latestValueRef.current = valueRef.current;
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-04-04 17:00:40 -05:00
|
|
|
const filteredMentions = useMemo<AutocompleteOption[]>(() => {
|
|
|
|
|
if (!mentionState) return [];
|
|
|
|
|
const q = mentionState.query.trim().toLowerCase();
|
|
|
|
|
if (mentionState.trigger === "skill") {
|
|
|
|
|
return slashCommands
|
|
|
|
|
.filter((command) => {
|
|
|
|
|
if (!q) return true;
|
|
|
|
|
return command.aliases.some((alias) => alias.toLowerCase().includes(q));
|
|
|
|
|
})
|
|
|
|
|
.slice(0, 8);
|
|
|
|
|
}
|
|
|
|
|
if (!mentions) return [];
|
2026-02-20 13:35:15 -06:00
|
|
|
return mentions.filter((m) => m.name.toLowerCase().includes(q)).slice(0, 8);
|
2026-04-04 17:00:40 -05:00
|
|
|
}, [mentionState, mentions, slashCommands]);
|
2026-02-20 13:35:15 -06:00
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
useImperativeHandle(forwardedRef, () => ({
|
|
|
|
|
focus: () => {
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
},
|
|
|
|
|
}), []);
|
|
|
|
|
|
2026-03-03 09:36:49 -06:00
|
|
|
// Whether the image plugin should be included (boolean is stable across renders
|
|
|
|
|
// as long as the handler presence doesn't toggle)
|
|
|
|
|
const hasImageUpload = Boolean(imageUploadHandler);
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const plugins = useMemo<RealmPlugin[]>(() => {
|
2026-03-03 09:36:49 -06:00
|
|
|
const imageHandler = hasImageUpload
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
? async (file: File) => {
|
2026-03-03 09:36:49 -06:00
|
|
|
const handler = imageUploadHandlerRef.current;
|
|
|
|
|
if (!handler) throw new Error("No image upload handler");
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
try {
|
2026-03-03 09:36:49 -06:00
|
|
|
const src = await handler(file);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
setUploadError(null);
|
2026-03-16 16:50:18 -05:00
|
|
|
// After MDXEditor inserts the image, ensure two newlines follow it
|
|
|
|
|
// so the cursor isn't stuck right next to the image.
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const current = latestValueRef.current;
|
2026-03-17 09:21:44 -05:00
|
|
|
const escapedSrc = escapeRegExp(src);
|
2026-03-16 16:50:18 -05:00
|
|
|
const updated = current.replace(
|
2026-03-17 09:21:44 -05:00
|
|
|
new RegExp(`(!\\[[^\\]]*\\]\\(${escapedSrc}\\))(?!\\n\\n)`, "g"),
|
|
|
|
|
"$1\n\n",
|
2026-03-16 16:50:18 -05:00
|
|
|
);
|
|
|
|
|
if (updated !== current) {
|
|
|
|
|
latestValueRef.current = updated;
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
echoIgnoreMarkdownRef.current = updated;
|
|
|
|
|
ref.current?.setMarkdown(updated);
|
2026-03-16 16:50:18 -05:00
|
|
|
onChange(updated);
|
|
|
|
|
requestAnimationFrame(() => {
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
2026-03-16 16:50:18 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
return src;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : "Image upload failed";
|
|
|
|
|
setUploadError(message);
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
const all: RealmPlugin[] = [
|
|
|
|
|
headingsPlugin(),
|
|
|
|
|
listsPlugin(),
|
|
|
|
|
quotePlugin(),
|
2026-02-26 16:33:48 -06:00
|
|
|
tablePlugin(),
|
2026-03-21 14:59:20 -05:00
|
|
|
linkPlugin({ validateUrl: isSafeMarkdownLinkUrl }),
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
linkDialogPlugin(),
|
2026-03-22 06:34:15 -05:00
|
|
|
mentionDeletionPlugin(),
|
fix: autoformat pasted markdown in inline editor (#2673)
## 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>
2026-04-04 11:21:27 -07:00
|
|
|
pasteNormalizationPlugin(),
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
thematicBreakPlugin(),
|
2026-03-02 16:44:03 -06:00
|
|
|
codeBlockPlugin({
|
|
|
|
|
defaultCodeBlockLanguage: "txt",
|
|
|
|
|
codeBlockEditorDescriptors: [FALLBACK_CODE_BLOCK_DESCRIPTOR],
|
|
|
|
|
}),
|
2026-02-26 16:33:48 -06:00
|
|
|
codeMirrorPlugin({ codeBlockLanguages: CODE_BLOCK_LANGUAGES }),
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
markdownShortcutPlugin(),
|
|
|
|
|
];
|
|
|
|
|
if (imageHandler) {
|
|
|
|
|
all.push(imagePlugin({ imageUploadHandler: imageHandler }));
|
|
|
|
|
}
|
|
|
|
|
return all;
|
2026-03-03 09:36:49 -06:00
|
|
|
}, [hasImageUpload]);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (value !== latestValueRef.current) {
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
if (ref.current) {
|
|
|
|
|
// Pair with onChange echo suppression (echoIgnoreMarkdownRef).
|
|
|
|
|
echoIgnoreMarkdownRef.current = value;
|
|
|
|
|
ref.current.setMarkdown(value);
|
|
|
|
|
latestValueRef.current = value;
|
2026-04-02 11:51:40 -05:00
|
|
|
}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}
|
|
|
|
|
}, [value]);
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
const decorateProjectMentions = useCallback(() => {
|
|
|
|
|
const editable = containerRef.current?.querySelector('[contenteditable="true"]');
|
|
|
|
|
if (!editable) return;
|
|
|
|
|
const links = editable.querySelectorAll("a");
|
|
|
|
|
for (const node of links) {
|
|
|
|
|
const link = node as HTMLAnchorElement;
|
2026-03-21 14:48:10 -05:00
|
|
|
const parsed = parseMentionChipHref(link.getAttribute("href") ?? "");
|
2026-03-02 13:31:58 -06:00
|
|
|
if (!parsed) {
|
2026-03-21 14:48:10 -05:00
|
|
|
clearMentionChipDecoration(link);
|
2026-03-02 13:31:58 -06:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
if (parsed.kind === "project") {
|
|
|
|
|
const option = mentionOptionByKey.get(`project:${parsed.projectId}`);
|
|
|
|
|
applyMentionChipDecoration(link, {
|
|
|
|
|
...parsed,
|
|
|
|
|
color: parsed.color ?? option?.projectColor ?? null,
|
|
|
|
|
});
|
|
|
|
|
continue;
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
2026-03-21 14:48:10 -05:00
|
|
|
|
2026-04-04 17:00:40 -05:00
|
|
|
if (parsed.kind === "skill") {
|
|
|
|
|
applyMentionChipDecoration(link, parsed);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
const option = mentionOptionByKey.get(`agent:${parsed.agentId}`);
|
|
|
|
|
applyMentionChipDecoration(link, {
|
|
|
|
|
...parsed,
|
|
|
|
|
icon: parsed.icon ?? option?.agentIcon ?? null,
|
|
|
|
|
});
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
2026-03-21 14:48:10 -05:00
|
|
|
}, [mentionOptionByKey]);
|
2026-03-02 13:31:58 -06:00
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
// Mention detection: listen for selection changes and input events
|
|
|
|
|
const checkMention = useCallback(() => {
|
2026-04-04 17:00:40 -05:00
|
|
|
if (!containerRef.current || isSelectionInsideCodeLikeElement(containerRef.current)) {
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
setMentionState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const result = detectMention(containerRef.current);
|
2026-04-04 17:00:40 -05:00
|
|
|
if (
|
|
|
|
|
result
|
|
|
|
|
&& result.trigger === "mention"
|
|
|
|
|
&& (!mentions || mentions.length === 0)
|
|
|
|
|
) {
|
|
|
|
|
mentionStateRef.current = null;
|
|
|
|
|
setMentionState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
result
|
|
|
|
|
&& result.trigger === "skill"
|
|
|
|
|
&& slashCommands.length === 0
|
|
|
|
|
) {
|
|
|
|
|
mentionStateRef.current = null;
|
|
|
|
|
setMentionState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = result;
|
2026-02-20 13:35:15 -06:00
|
|
|
if (result) {
|
|
|
|
|
setMentionState(result);
|
|
|
|
|
setMentionIndex(0);
|
|
|
|
|
} else {
|
|
|
|
|
setMentionState(null);
|
|
|
|
|
}
|
2026-04-04 17:00:40 -05:00
|
|
|
}, [mentions, slashCommands.length]);
|
2026-02-20 13:35:15 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-04 17:00:40 -05:00
|
|
|
if ((!mentions || mentions.length === 0) && slashCommands.length === 0) return;
|
2026-02-20 13:35:15 -06:00
|
|
|
|
2026-02-20 14:53:46 -06:00
|
|
|
const el = containerRef.current;
|
|
|
|
|
// Listen for input events on the container so mention detection
|
|
|
|
|
// also fires after typing (e.g. space to dismiss).
|
|
|
|
|
const onInput = () => requestAnimationFrame(checkMention);
|
|
|
|
|
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
2026-04-08 17:54:03 -07:00
|
|
|
let selRafId: number | null = null;
|
|
|
|
|
const onSelectionChange = () => {
|
|
|
|
|
if (selRafId !== null) return;
|
|
|
|
|
selRafId = requestAnimationFrame(() => {
|
|
|
|
|
selRafId = null;
|
|
|
|
|
checkMention();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener("selectionchange", onSelectionChange);
|
2026-02-20 14:53:46 -06:00
|
|
|
el?.addEventListener("input", onInput, true);
|
|
|
|
|
return () => {
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
2026-04-08 17:54:03 -07:00
|
|
|
document.removeEventListener("selectionchange", onSelectionChange);
|
2026-02-20 14:53:46 -06:00
|
|
|
el?.removeEventListener("input", onInput, true);
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
2026-04-08 17:54:03 -07:00
|
|
|
if (selRafId !== null) cancelAnimationFrame(selRafId);
|
2026-02-20 14:53:46 -06:00
|
|
|
};
|
2026-04-04 17:00:40 -05:00
|
|
|
}, [checkMention, mentions, slashCommands.length]);
|
2026-02-20 13:35:15 -06:00
|
|
|
|
2026-04-04 13:12:06 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!mentionActive) return;
|
|
|
|
|
|
|
|
|
|
const updatePosition = () => requestAnimationFrame(checkMention);
|
|
|
|
|
const viewport = window.visualViewport;
|
|
|
|
|
|
|
|
|
|
viewport?.addEventListener("resize", updatePosition);
|
|
|
|
|
viewport?.addEventListener("scroll", updatePosition);
|
|
|
|
|
window.addEventListener("resize", updatePosition);
|
|
|
|
|
window.addEventListener("scroll", updatePosition, true);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
viewport?.removeEventListener("resize", updatePosition);
|
|
|
|
|
viewport?.removeEventListener("scroll", updatePosition);
|
|
|
|
|
window.removeEventListener("resize", updatePosition);
|
|
|
|
|
window.removeEventListener("scroll", updatePosition, true);
|
|
|
|
|
};
|
|
|
|
|
}, [checkMention, mentionActive]);
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
const editable = containerRef.current?.querySelector('[contenteditable="true"]');
|
|
|
|
|
if (!editable) return;
|
|
|
|
|
decorateProjectMentions();
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
2026-04-08 17:54:03 -07:00
|
|
|
let rafId: number | null = null;
|
2026-03-02 13:31:58 -06:00
|
|
|
const observer = new MutationObserver(() => {
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
2026-04-08 17:54:03 -07:00
|
|
|
if (rafId !== null) return;
|
|
|
|
|
rafId = requestAnimationFrame(() => {
|
|
|
|
|
rafId = null;
|
|
|
|
|
decorateProjectMentions();
|
|
|
|
|
});
|
2026-03-02 13:31:58 -06:00
|
|
|
});
|
|
|
|
|
observer.observe(editable, {
|
|
|
|
|
subtree: true,
|
|
|
|
|
childList: true,
|
|
|
|
|
characterData: true,
|
|
|
|
|
});
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
2026-04-08 17:54:03 -07:00
|
|
|
return () => {
|
|
|
|
|
observer.disconnect();
|
|
|
|
|
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
|
|
|
};
|
|
|
|
|
}, [decorateProjectMentions]);
|
2026-03-02 13:31:58 -06:00
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
const selectMention = useCallback(
|
2026-04-04 17:00:40 -05:00
|
|
|
(option: AutocompleteOption) => {
|
2026-02-20 14:59:20 -06:00
|
|
|
// Read from ref to avoid stale-closure issues (selectionchange can
|
|
|
|
|
// update state between the last render and this callback firing).
|
|
|
|
|
const state = mentionStateRef.current;
|
|
|
|
|
if (!state) return;
|
2026-03-21 14:48:10 -05:00
|
|
|
const current = latestValueRef.current;
|
2026-04-04 17:00:40 -05:00
|
|
|
const next = applyMention(current, state, option);
|
2026-03-21 14:48:10 -05:00
|
|
|
if (next !== current) {
|
|
|
|
|
latestValueRef.current = next;
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
echoIgnoreMarkdownRef.current = next;
|
|
|
|
|
ref.current?.setMarkdown(next);
|
2026-03-21 14:48:10 -05:00
|
|
|
onChange(next);
|
|
|
|
|
}
|
2026-02-20 16:07:37 -06:00
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
requestAnimationFrame(() => {
|
2026-03-02 13:32:15 -06:00
|
|
|
requestAnimationFrame(() => {
|
2026-03-21 14:48:10 -05:00
|
|
|
const editable = containerRef.current?.querySelector('[contenteditable="true"]');
|
|
|
|
|
if (!(editable instanceof HTMLElement)) return;
|
2026-03-02 13:32:15 -06:00
|
|
|
decorateProjectMentions();
|
2026-03-21 14:48:10 -05:00
|
|
|
editable.focus();
|
2026-03-02 13:32:15 -06:00
|
|
|
|
2026-04-04 17:00:40 -05:00
|
|
|
const mentionHref = option.kind === "skill"
|
|
|
|
|
? option.href
|
|
|
|
|
: option.kind === "project" && option.projectId
|
|
|
|
|
? buildProjectMentionHref(option.projectId, option.projectColor ?? null)
|
|
|
|
|
: buildAgentMentionHref(
|
|
|
|
|
option.agentId ?? option.id.replace(/^agent:/, ""),
|
|
|
|
|
option.agentIcon ?? null,
|
|
|
|
|
);
|
|
|
|
|
const expectedLabel = option.kind === "skill" ? `/${option.slug}` : `@${option.name}`;
|
2026-03-21 14:48:10 -05:00
|
|
|
const matchingMentions = Array.from(editable.querySelectorAll("a"))
|
|
|
|
|
.filter((node): node is HTMLAnchorElement => node instanceof HTMLAnchorElement)
|
|
|
|
|
.filter((link) => {
|
|
|
|
|
const href = link.getAttribute("href") ?? "";
|
2026-04-04 17:00:40 -05:00
|
|
|
return href === mentionHref && link.textContent === expectedLabel;
|
2026-03-21 14:48:10 -05:00
|
|
|
});
|
|
|
|
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
|
|
|
const target = matchingMentions.sort((a, b) => {
|
|
|
|
|
const rectA = a.getBoundingClientRect();
|
|
|
|
|
const rectB = b.getBoundingClientRect();
|
|
|
|
|
const leftA = containerRect ? rectA.left - containerRect.left : rectA.left;
|
|
|
|
|
const topA = containerRect ? rectA.top - containerRect.top : rectA.top;
|
|
|
|
|
const leftB = containerRect ? rectB.left - containerRect.left : rectB.left;
|
|
|
|
|
const topB = containerRect ? rectB.top - containerRect.top : rectB.top;
|
|
|
|
|
const distA = Math.hypot(leftA - state.left, topA - state.top);
|
|
|
|
|
const distB = Math.hypot(leftB - state.left, topB - state.top);
|
|
|
|
|
return distA - distB;
|
|
|
|
|
})[0] ?? null;
|
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
const selection = window.getSelection();
|
|
|
|
|
if (!selection) return;
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
const nextSibling = target.nextSibling;
|
|
|
|
|
if (nextSibling?.nodeType === Node.TEXT_NODE) {
|
|
|
|
|
const text = nextSibling.textContent ?? "";
|
|
|
|
|
if (text.startsWith(" ")) {
|
|
|
|
|
range.setStart(nextSibling, 1);
|
|
|
|
|
range.collapse(true);
|
|
|
|
|
selection.removeAllRanges();
|
|
|
|
|
selection.addRange(range);
|
2026-02-20 16:07:37 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
range.setStartAfter(target);
|
|
|
|
|
range.collapse(true);
|
|
|
|
|
selection.removeAllRanges();
|
|
|
|
|
selection.addRange(range);
|
|
|
|
|
});
|
2026-03-02 13:31:58 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
setMentionState(null);
|
|
|
|
|
},
|
2026-03-02 13:31:58 -06:00
|
|
|
[decorateProjectMentions, onChange],
|
2026-02-20 13:35:15 -06:00
|
|
|
);
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
function hasFilePayload(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
return Array.from(evt.dataTransfer?.types ?? []).includes("Files");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const canDropImage = Boolean(imageUploadHandler);
|
2026-04-05 06:39:20 -05:00
|
|
|
const canDropFile = Boolean(imageUploadHandler || onDropFile);
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
const handlePasteCapture = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
|
|
|
|
const clipboard = event.clipboardData;
|
|
|
|
|
if (!clipboard || !ref.current) return;
|
|
|
|
|
const types = new Set(Array.from(clipboard.types));
|
|
|
|
|
if (types.has("Files") || types.has("text/html")) return;
|
|
|
|
|
if (isSelectionInsideCodeLikeElement(containerRef.current)) return;
|
|
|
|
|
|
|
|
|
|
const rawText = clipboard.getData("text/plain");
|
|
|
|
|
if (!looksLikeMarkdownPaste(rawText)) return;
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
fix: autoformat pasted markdown in inline editor (#2673)
## 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>
2026-04-04 11:21:27 -07:00
|
|
|
ref.current.insertMarkdown(normalizeMarkdown(rawText));
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
}, []);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
|
2026-04-04 13:12:06 -05:00
|
|
|
const mentionMenuPosition = mentionState
|
2026-04-04 19:10:58 -05:00
|
|
|
? computeMentionMenuPosition(
|
|
|
|
|
mentionState,
|
|
|
|
|
getMentionMenuViewport(),
|
|
|
|
|
getMentionMenuSize(filteredMentions.length),
|
|
|
|
|
)
|
2026-04-04 13:12:06 -05:00
|
|
|
: null;
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
className={cn(
|
|
|
|
|
"relative paperclip-mdxeditor-scope",
|
|
|
|
|
bordered ? "rounded-md border border-border bg-transparent" : "bg-transparent",
|
|
|
|
|
isDragOver && "ring-1 ring-primary/60 bg-accent/20",
|
|
|
|
|
className,
|
|
|
|
|
)}
|
2026-02-20 13:35:15 -06:00
|
|
|
onKeyDownCapture={(e) => {
|
|
|
|
|
// Cmd/Ctrl+Enter to submit
|
|
|
|
|
if (onSubmit && e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onSubmit();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:59:20 -06:00
|
|
|
// Mention keyboard handling
|
|
|
|
|
if (mentionActive) {
|
2026-04-04 19:10:58 -05:00
|
|
|
if (e.key === " " && mentionStateRef.current?.trigger === "skill") {
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
|
|
|
|
setMentionState(null);
|
2026-02-20 13:35:15 -06:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 14:59:20 -06:00
|
|
|
// Escape always dismisses
|
2026-02-20 13:35:15 -06:00
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
setMentionState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 14:59:20 -06:00
|
|
|
// Arrow / Enter / Tab only when there are filtered results
|
|
|
|
|
if (filteredMentions.length > 0) {
|
|
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setMentionIndex((prev) => Math.min(prev + 1, filteredMentions.length - 1));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "ArrowUp") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setMentionIndex((prev) => Math.max(prev - 1, 0));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Enter" || e.key === "Tab") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
selectMention(filteredMentions[mentionIndex]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
}}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
onDragEnter={(evt) => {
|
2026-04-05 06:39:20 -05:00
|
|
|
if (!canDropFile || !hasFilePayload(evt)) return;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
dragDepthRef.current += 1;
|
|
|
|
|
setIsDragOver(true);
|
|
|
|
|
}}
|
|
|
|
|
onDragOver={(evt) => {
|
2026-04-05 06:39:20 -05:00
|
|
|
if (!canDropFile || !hasFilePayload(evt)) return;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.dataTransfer.dropEffect = "copy";
|
|
|
|
|
}}
|
2026-02-20 13:35:15 -06:00
|
|
|
onDragLeave={() => {
|
2026-04-05 06:39:20 -05:00
|
|
|
if (!canDropFile) return;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
|
|
|
|
if (dragDepthRef.current === 0) setIsDragOver(false);
|
|
|
|
|
}}
|
2026-04-05 06:39:20 -05:00
|
|
|
onDrop={(evt) => {
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
dragDepthRef.current = 0;
|
|
|
|
|
setIsDragOver(false);
|
2026-04-05 06:39:20 -05:00
|
|
|
if (!onDropFile) return;
|
|
|
|
|
const files = evt.dataTransfer?.files;
|
|
|
|
|
if (!files || files.length === 0) return;
|
|
|
|
|
const allFiles = Array.from(files);
|
|
|
|
|
const nonImageFiles = allFiles.filter(
|
|
|
|
|
(f) => !f.type.startsWith("image/"),
|
|
|
|
|
);
|
|
|
|
|
if (nonImageFiles.length === 0) return;
|
|
|
|
|
// If all dropped files are non-image, prevent default so MDXEditor
|
|
|
|
|
// doesn't try to handle them. If mixed, let images flow through to
|
|
|
|
|
// the image plugin and only handle the non-image files ourselves.
|
|
|
|
|
if (nonImageFiles.length === allFiles.length) {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.stopPropagation();
|
|
|
|
|
}
|
|
|
|
|
for (const file of nonImageFiles) {
|
|
|
|
|
void onDropFile(file);
|
|
|
|
|
}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}}
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
onPasteCapture={handlePasteCapture}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
>
|
|
|
|
|
<MDXEditor
|
2026-04-09 06:12:43 -05:00
|
|
|
ref={setEditorRef}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
markdown={value}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
onChange={(next) => {
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
const echo = echoIgnoreMarkdownRef.current;
|
|
|
|
|
if (echo !== null && next === echo) {
|
|
|
|
|
echoIgnoreMarkdownRef.current = null;
|
|
|
|
|
latestValueRef.current = next;
|
2026-04-02 11:51:40 -05:00
|
|
|
return;
|
|
|
|
|
}
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
if (echo !== null) {
|
|
|
|
|
echoIgnoreMarkdownRef.current = null;
|
|
|
|
|
}
|
2026-04-02 11:51:40 -05:00
|
|
|
|
Fix markdown paste handling in document editor (#2572)
Supersedes #2499.
## Thinking Path
1. **Project context**: Paperclip uses a markdown editor
(`MarkdownEditor`) for document editing. Users expect to paste
markdown-formatted text from external sources (like code editors, other
documents) and have it render correctly.
2. **Problem identification**: When users paste plain text containing
markdown syntax (e.g., `# Heading`, `- list item`), the editor was
treating it as plain text, resulting in raw markdown syntax being
displayed rather than formatted content.
3. **Root cause**: The default browser paste behavior doesn't recognize
markdown syntax in plain text. The editor needed to intercept paste
events and detect when the clipboard content looks like markdown.
4. **Solution design**:
- Create a utility (`markdownPaste.ts`) to detect markdown patterns in
plain text
- Add a paste capture handler in `MarkdownEditor` that intercepts paste
events
- When markdown is detected, prevent default paste and use
`insertMarkdown` instead
- Handle edge cases (code blocks, file pastes, HTML content)
## What
- Added `ui/src/lib/markdownPaste.ts`: Utility to detect markdown
patterns and normalize line endings
- Added `ui/src/lib/markdownPaste.test.ts`: Test coverage for markdown
detection
- Modified `ui/src/components/MarkdownEditor.tsx`: Added paste capture
handler to intercept and handle markdown paste
## Why
Users frequently copy markdown content from various sources (GitHub,
documentation, notes) and expect it to render correctly when pasted into
the editor. Without this fix, users see raw markdown syntax (e.g., `#
Title` instead of a formatted heading), which degrades the editing
experience.
## How to Verify
1. Open any document in Paperclip
2. Copy markdown text from an external source (e.g., `# Heading\n\n-
Item 1\n- Item 2`)
3. Paste into the editor
4. **Expected**: The content should render as formatted markdown
(heading + bullet list), not as plain text with markdown syntax
### Test Coverage
```bash
cd ui
npm test -- markdownPaste.test.ts
```
All tests should pass, including:
- Windows line ending normalization (`\r\n` → `\n`)
- Old-Mac line ending normalization (`\r` → `\n`)
- Markdown block detection (headings, lists, code fences, etc.)
- Plain text rejection (non-markdown content)
## Risks
1. **False positives**: Plain text containing markdown-like characters
(e.g., a paragraph starting with `#` as a hashtag) may be incorrectly
treated as markdown. The detection uses a heuristic that requires
block-level markdown patterns, which reduces but doesn't eliminate this
risk.
2. **Removed focus guard**: The previous implementation used
`isFocusedRef` to prevent `onChange` from firing during programmatic
`setMarkdown` calls. This guard was removed as part of refactoring. The
assumption is that MDXEditor does not fire `onChange` during
`setMarkdown`, but this should be monitored for unexpected parent update
loops.
3. **Clipboard compatibility**: The paste handler specifically looks for
`text/plain` content and ignores `text/html` (to preserve existing HTML
paste behavior). This means pasting from rich text editors that provide
both HTML and plain text will continue to use the HTML path, which may
or may not be the desired behavior.
---------
Co-authored-by: 馨冉 <xinxincui239@gmail.com>
2026-04-03 23:50:48 +08:00
|
|
|
if (initialChildOnChangeRef.current) {
|
|
|
|
|
initialChildOnChangeRef.current = false;
|
|
|
|
|
if (next === "" && value !== "") {
|
|
|
|
|
echoIgnoreMarkdownRef.current = value;
|
|
|
|
|
ref.current?.setMarkdown(value);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
latestValueRef.current = next;
|
|
|
|
|
onChange(next);
|
|
|
|
|
}}
|
|
|
|
|
onBlur={() => onBlur?.()}
|
|
|
|
|
className={cn("paperclip-mdxeditor", !bordered && "paperclip-mdxeditor--borderless")}
|
|
|
|
|
contentEditableClassName={cn(
|
2026-03-04 12:20:29 -06:00
|
|
|
"paperclip-mdxeditor-content focus:outline-none [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:list-item",
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
contentClassName,
|
|
|
|
|
)}
|
2026-03-23 20:41:50 -05:00
|
|
|
additionalLexicalNodes={[MentionAwareLinkNode, mentionAwareLinkNodeReplacement]}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
plugins={plugins}
|
|
|
|
|
/>
|
2026-02-20 13:35:15 -06:00
|
|
|
|
2026-03-26 07:22:24 -05:00
|
|
|
{/* Mention dropdown — rendered via portal so it isn't clipped by overflow containers */}
|
|
|
|
|
{mentionActive && filteredMentions.length > 0 &&
|
|
|
|
|
createPortal(
|
|
|
|
|
<div
|
2026-03-26 19:18:38 -05:00
|
|
|
className="fixed z-[9999] min-w-[180px] max-w-[calc(100vw-16px)] max-h-[200px] overflow-y-auto rounded-md border border-border bg-popover shadow-md"
|
2026-04-04 13:12:06 -05:00
|
|
|
style={mentionMenuPosition ?? undefined}
|
2026-03-26 07:22:24 -05:00
|
|
|
>
|
|
|
|
|
{filteredMentions.map((option, i) => (
|
|
|
|
|
<button
|
|
|
|
|
key={option.id}
|
2026-04-04 13:12:06 -05:00
|
|
|
type="button"
|
2026-03-26 07:22:24 -05:00
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-3 py-1.5 text-sm text-left hover:bg-accent/50 transition-colors",
|
|
|
|
|
i === mentionIndex && "bg-accent",
|
|
|
|
|
)}
|
2026-04-04 13:12:06 -05:00
|
|
|
onPointerDown={(e) => {
|
2026-03-26 07:22:24 -05:00
|
|
|
e.preventDefault(); // prevent blur
|
|
|
|
|
selectMention(option);
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={() => setMentionIndex(i)}
|
|
|
|
|
>
|
2026-04-04 17:00:40 -05:00
|
|
|
{option.kind === "skill" ? (
|
|
|
|
|
<Boxes className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
) : option.kind === "project" && option.projectId ? (
|
2026-03-26 07:22:24 -05:00
|
|
|
<span
|
|
|
|
|
className="inline-flex h-2 w-2 rounded-full border border-border/50"
|
|
|
|
|
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<AgentIcon
|
|
|
|
|
icon={option.agentIcon}
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-04-04 17:00:40 -05:00
|
|
|
<span>{option.kind === "skill" ? `/${option.slug}` : option.name}</span>
|
2026-03-26 07:22:24 -05:00
|
|
|
{option.kind === "project" && option.projectId && (
|
|
|
|
|
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
Project
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-04-04 17:00:40 -05:00
|
|
|
{option.kind === "skill" && (
|
|
|
|
|
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
Skill
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-03-26 07:22:24 -05:00
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>,
|
|
|
|
|
document.body,
|
|
|
|
|
)}
|
2026-02-20 13:35:15 -06:00
|
|
|
|
2026-04-05 06:39:20 -05:00
|
|
|
{isDragOver && canDropFile && (
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"pointer-events-none absolute inset-1 z-40 flex items-center justify-center rounded-md border border-dashed border-primary/80 bg-primary/10 text-xs font-medium text-primary",
|
|
|
|
|
!bordered && "inset-0 rounded-sm",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-04-06 21:56:13 -05:00
|
|
|
Drop {onDropFile ? "file" : "image"} to upload
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{uploadError && (
|
|
|
|
|
<p className="px-3 pb-2 text-xs text-destructive">{uploadError}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|