forked from farhoodlabs/paperclip
[codex] Polish issue composer and long document display (#4420)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Issue comments and documents are the main working surface where operators and agents collaborate > - File drops, markdown editing, and long issue descriptions need to feel predictable because they sit directly in the task execution loop > - The composer had edge cases around drag targets, attachment feedback, image drops, and long markdown content crowding the page > - This pull request polishes the issue composer, hardens markdown editor regressions, and adds a fold curtain for long issue descriptions/documents > - The benefit is a calmer issue detail surface that handles uploads and long work products without hiding state or breaking layout ## What Changed - Scoped issue-composer drag/drop behavior so the composer owns file drops without turning the whole thread into a competing drop target. - Added clearer attachment upload feedback for non-image files and image-drop stability coverage. - Hardened markdown editor and markdown body handling around HTML-like tag regressions. - Added `FoldCurtain` and wired it into issue descriptions and issue documents so long markdown previews can expand/collapse. - Added Storybook coverage for the fold curtain state. ## Verification - `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx ui/src/components/MarkdownEditor.test.tsx ui/src/components/MarkdownBody.test.tsx --config ui/vitest.config.ts` passed: 3 files, 75 tests. - `git diff --check public-gh/master..pap-2228-editor-composer-polish -- . ':(exclude)ui/storybook-static'` passed. - Confirmed this PR does not include `pnpm-lock.yaml`. ## Risks - Low-to-medium risk: this changes user-facing composer/drop behavior and long markdown display. - The fold curtain uses DOM measurement and `ResizeObserver`; reviewers should check browser behavior for very long descriptions and documents. - No database migrations. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent based on GPT-5, with shell, git, Paperclip API, and GitHub CLI tool use in the local Paperclip workspace. ## 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 checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [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 Note: screenshots were not newly captured during branch splitting; the UI states are covered by component tests and a Storybook story. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -68,6 +68,8 @@ interface MarkdownEditorProps {
|
||||
imageUploadHandler?: (file: File) => Promise<string>;
|
||||
/** Called when a non-image file is dropped onto the editor (e.g. .zip). */
|
||||
onDropFile?: (file: File) => Promise<void>;
|
||||
/** When set to `parent`, a wrapper owns drag/drop behavior and visuals. */
|
||||
fileDropTarget?: "editor" | "parent";
|
||||
bordered?: boolean;
|
||||
/** List of mentionable entities. Enables @-mention autocomplete. */
|
||||
mentions?: MentionOption[];
|
||||
@@ -126,6 +128,10 @@ function hasMeaningfulEditorContent(node: Node | null): boolean {
|
||||
return Array.from(element.childNodes).some((child) => hasMeaningfulEditorContent(child));
|
||||
}
|
||||
|
||||
function hasMarkdownImage(value: string): boolean {
|
||||
return /!\[[\s\S]*?\]\([^)]+\)/.test(value);
|
||||
}
|
||||
|
||||
function isRichEditorDomEmpty(
|
||||
editable: HTMLElement,
|
||||
expectedValue: string,
|
||||
@@ -133,9 +139,11 @@ function isRichEditorDomEmpty(
|
||||
): boolean {
|
||||
const expectedText = expectedValue.trim();
|
||||
if (!expectedText) return false;
|
||||
const expectedHasImage = hasMarkdownImage(expectedText);
|
||||
|
||||
const visibleText = (editable.textContent ?? "").trim();
|
||||
if (visibleText.length === 0) {
|
||||
if (expectedHasImage) return false;
|
||||
return !Array.from(editable.childNodes).some((child) => hasMeaningfulEditorContent(child));
|
||||
}
|
||||
|
||||
@@ -145,6 +153,7 @@ function isRichEditorDomEmpty(
|
||||
&& visibleText === normalizedPlaceholder
|
||||
&& expectedText !== normalizedPlaceholder
|
||||
) {
|
||||
if (expectedHasImage) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -491,6 +500,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||
onBlur,
|
||||
imageUploadHandler,
|
||||
onDropFile,
|
||||
fileDropTarget = "editor",
|
||||
bordered = true,
|
||||
mentions,
|
||||
onSubmit,
|
||||
@@ -897,8 +907,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||
return Array.from(evt.dataTransfer?.types ?? []).includes("Files");
|
||||
}
|
||||
|
||||
const canDropImage = Boolean(imageUploadHandler);
|
||||
const canDropFile = Boolean(imageUploadHandler || onDropFile);
|
||||
const canDropFile = fileDropTarget === "editor" && Boolean(imageUploadHandler || onDropFile);
|
||||
const handlePasteCapture = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
||||
const clipboard = event.clipboardData;
|
||||
if (!clipboard || !ref.current) return;
|
||||
@@ -1082,6 +1091,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||
<MDXEditor
|
||||
ref={setEditorRef}
|
||||
markdown={editorValue}
|
||||
suppressHtmlProcessing
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
onChange={(next) => {
|
||||
|
||||
Reference in New Issue
Block a user