[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:
Dotta 2026-04-24 14:12:41 -05:00 committed by GitHub
parent 8f1cd0474f
commit 77a72e28c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 839 additions and 54 deletions

View file

@ -4,6 +4,7 @@ import type { Agent, CompanySecret, EnvBinding, Project, RoutineVariable } from
import { Code2, FileText, ListPlus, RotateCcw, Table2 } from "lucide-react";
import { EnvVarEditor } from "@/components/EnvVarEditor";
import { ExecutionParticipantPicker } from "@/components/ExecutionParticipantPicker";
import { FoldCurtain } from "@/components/FoldCurtain";
import { InlineEditor } from "@/components/InlineEditor";
import { InlineEntitySelector, type InlineEntityOption } from "@/components/InlineEntitySelector";
import { JsonSchemaForm, type JsonSchemaNode, getDefaultValues } from "@/components/JsonSchemaForm";
@ -710,3 +711,85 @@ export const RoutineRunVariablesDialogOpen: Story = {
name: "Routine Run Variables Dialog",
render: () => <RoutineRunDialogStory />,
};
const foldCurtainLongMarkdown = [
"# paperclip-bench",
"",
"Ship criteria for the benchmark harness — these notes are intentionally lengthy so the fold-curtain clips them.",
"",
"## Overview",
"",
"We need a benchmark that compares agent performance across task types and model backends. This includes:",
"",
"- a **runner** that executes tasks in isolated workspaces",
"- a **scorer** that grades outputs against ground truth",
"- a **dashboard** that trends metrics over time",
"",
"## Task format",
"",
"Each task is a directory containing a `task.md`, an optional `setup.sh`, and an `expected/` fixture. The runner mounts the task, executes the agent, and diffs the resulting workspace against `expected/`.",
"",
"```ts",
"type TaskResult = {",
" taskId: string;",
" agent: string;",
" exitCode: number;",
" scoreBreakdown: Record<string, number>;",
"};",
"```",
"",
"## Metrics",
"",
"| Metric | Description |",
"| --- | --- |",
"| Pass@1 | First-try correctness |",
"| Tokens | Cost per task |",
"| Wall time | End-to-end minutes |",
"",
"## Next steps",
"",
"1. Land the runner with support for 3 task types.",
"2. Backfill 50 tasks from open-source benchmarks.",
"3. Wire the scorer to GitHub Actions.",
"4. Publish baseline numbers on the main branch.",
"",
"All of this is described in more detail in the design doc linked from the home page.",
].join("\n");
const foldCurtainShortMarkdown = "This description is short. No curtain should appear.";
function FoldCurtainStory() {
return (
<StoryShell>
<Section
eyebrow="Presentation"
title="FoldCurtain"
description="Long content collapses to a preview with a bottom fade and a Show more button. Short content renders untouched."
>
<div className="space-y-6">
<StatePanel
label="Long description (collapsed)"
detail="Default state on every fresh page load. Natural height far exceeds the collapsed height, so the curtain activates."
>
<FoldCurtain>
<MarkdownBody className="text-[15px] leading-7">{foldCurtainLongMarkdown}</MarkdownBody>
</FoldCurtain>
</StatePanel>
<StatePanel
label="Short description (no curtain)"
detail="Content below the activation threshold renders with no curtain and no button."
>
<FoldCurtain>
<MarkdownBody className="text-[15px] leading-7">{foldCurtainShortMarkdown}</MarkdownBody>
</FoldCurtain>
</StatePanel>
</div>
</Section>
</StoryShell>
);
}
export const FoldCurtainShowcase: Story = {
name: "Fold Curtain",
render: () => <FoldCurtainStory />,
};