2026-03-19 16:41:28 -05:00
|
|
|
// @vitest-environment node
|
|
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
import type { ReactNode } from "react";
|
|
|
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
2026-03-19 16:41:28 -05:00
|
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
2026-04-04 17:00:40 -05:00
|
|
|
import { buildAgentMentionHref, buildProjectMentionHref, buildSkillMentionHref } from "@paperclipai/shared";
|
2026-03-19 16:41:28 -05:00
|
|
|
import { ThemeProvider } from "../context/ThemeContext";
|
|
|
|
|
import { MarkdownBody } from "./MarkdownBody";
|
2026-04-10 22:26:21 -05:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
|
|
|
|
|
|
|
|
const mockIssuesApi = vi.hoisted(() => ({
|
|
|
|
|
get: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/router", () => ({
|
|
|
|
|
Link: ({ children, to }: { children: ReactNode; to: string }) => <a href={to}>{children}</a>,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../api/issues", () => ({
|
|
|
|
|
issuesApi: mockIssuesApi,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
function renderMarkdown(children: string, seededIssues: Array<{ identifier: string; status: string }> = []) {
|
|
|
|
|
const queryClient = new QueryClient({
|
|
|
|
|
defaultOptions: {
|
|
|
|
|
queries: {
|
|
|
|
|
retry: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const issue of seededIssues) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.detail(issue.identifier), {
|
|
|
|
|
id: issue.identifier,
|
|
|
|
|
identifier: issue.identifier,
|
|
|
|
|
status: issue.status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return renderToStaticMarkup(
|
|
|
|
|
<QueryClientProvider client={queryClient}>
|
|
|
|
|
<ThemeProvider>
|
|
|
|
|
<MarkdownBody>{children}</MarkdownBody>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
</QueryClientProvider>,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-19 16:41:28 -05:00
|
|
|
|
|
|
|
|
describe("MarkdownBody", () => {
|
|
|
|
|
it("renders markdown images without a resolver", () => {
|
|
|
|
|
const html = renderToStaticMarkup(
|
2026-04-10 22:26:21 -05:00
|
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
|
|
|
<ThemeProvider>
|
|
|
|
|
<MarkdownBody>{""}</MarkdownBody>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
</QueryClientProvider>,
|
2026-03-19 16:41:28 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('<img src="/api/attachments/test/content" alt=""/>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("resolves relative image paths when a resolver is provided", () => {
|
|
|
|
|
const html = renderToStaticMarkup(
|
2026-04-10 22:26:21 -05:00
|
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
|
|
|
<ThemeProvider>
|
|
|
|
|
<MarkdownBody resolveImageSrc={(src) => `/resolved/${src}`}>
|
|
|
|
|
{""}
|
|
|
|
|
</MarkdownBody>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
</QueryClientProvider>,
|
2026-03-19 16:41:28 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('src="/resolved/images/org-chart.png"');
|
|
|
|
|
expect(html).toContain('alt="Org chart"');
|
|
|
|
|
});
|
2026-03-21 14:48:10 -05:00
|
|
|
|
2026-04-04 17:00:40 -05:00
|
|
|
it("renders agent, project, and skill mentions as chips", () => {
|
2026-03-21 14:48:10 -05:00
|
|
|
const html = renderToStaticMarkup(
|
2026-04-10 22:26:21 -05:00
|
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
|
|
|
<ThemeProvider>
|
|
|
|
|
<MarkdownBody>
|
|
|
|
|
{`[@CodexCoder](${buildAgentMentionHref("agent-123", "code")}) [@Paperclip App](${buildProjectMentionHref("project-456", "#336699")}) [/release-changelog](${buildSkillMentionHref("skill-789", "release-changelog")})`}
|
|
|
|
|
</MarkdownBody>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
</QueryClientProvider>,
|
2026-03-21 14:48:10 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('href="/agents/agent-123"');
|
|
|
|
|
expect(html).toContain('data-mention-kind="agent"');
|
|
|
|
|
expect(html).toContain("--paperclip-mention-icon-mask");
|
|
|
|
|
expect(html).toContain('href="/projects/project-456"');
|
|
|
|
|
expect(html).toContain('data-mention-kind="project"');
|
|
|
|
|
expect(html).toContain("--paperclip-mention-project-color:#336699");
|
2026-04-04 17:00:40 -05:00
|
|
|
expect(html).toContain('href="/skills/skill-789"');
|
|
|
|
|
expect(html).toContain('data-mention-kind="skill"');
|
2026-03-21 14:48:10 -05:00
|
|
|
});
|
2026-04-10 22:26:21 -05:00
|
|
|
|
|
|
|
|
it("uses soft-break styling by default", () => {
|
|
|
|
|
const html = renderMarkdown("First line\nSecond line");
|
|
|
|
|
|
|
|
|
|
expect(html).toContain("First line<br/>");
|
|
|
|
|
expect(html).toContain("Second line");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("can opt out of soft-break styling", () => {
|
|
|
|
|
const html = renderToStaticMarkup(
|
|
|
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
|
|
|
<ThemeProvider>
|
|
|
|
|
<MarkdownBody softBreaks={false}>
|
|
|
|
|
{"First line\nSecond line"}
|
|
|
|
|
</MarkdownBody>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
</QueryClientProvider>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(html).not.toContain("<br/>");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not inject extra line-break nodes into nested lists", () => {
|
|
|
|
|
const html = renderMarkdown("1. Parent item\n - child a\n - child b\n\n2. Second item");
|
|
|
|
|
|
|
|
|
|
expect(html).not.toContain("[&_p]:whitespace-pre-line");
|
|
|
|
|
expect(html).not.toContain("Parent item<br/>");
|
|
|
|
|
expect(html).toContain("<ol>");
|
|
|
|
|
expect(html).toContain("<ul>");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("linkifies bare issue identifiers in markdown text", () => {
|
|
|
|
|
const html = renderMarkdown("Depends on PAP-1271 for the hover state.", [
|
|
|
|
|
{ identifier: "PAP-1271", status: "done" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('href="/issues/PAP-1271"');
|
|
|
|
|
expect(html).toContain("text-green-600");
|
|
|
|
|
expect(html).toContain(">PAP-1271<");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rewrites full issue URLs to internal issue links", () => {
|
|
|
|
|
const html = renderMarkdown("See http://localhost:3100/PAP/issues/PAP-1179.", [
|
|
|
|
|
{ identifier: "PAP-1179", status: "blocked" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('href="/issues/PAP-1179"');
|
|
|
|
|
expect(html).toContain("text-red-600");
|
|
|
|
|
expect(html).toContain(">http://localhost:3100/PAP/issues/PAP-1179<");
|
|
|
|
|
});
|
|
|
|
|
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] 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-14 12:50:48 -05:00
|
|
|
it("rewrites issue scheme links to internal issue links", () => {
|
|
|
|
|
const html = renderMarkdown("See issue://PAP-1310 and issue://:PAP-1311.", [
|
|
|
|
|
{ identifier: "PAP-1310", status: "done" },
|
|
|
|
|
{ identifier: "PAP-1311", status: "blocked" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('href="/issues/PAP-1310"');
|
|
|
|
|
expect(html).toContain('href="/issues/PAP-1311"');
|
|
|
|
|
expect(html).toContain(">issue://PAP-1310<");
|
|
|
|
|
expect(html).toContain(">issue://:PAP-1311<");
|
|
|
|
|
expect(html).toContain("text-green-600");
|
|
|
|
|
expect(html).toContain("text-red-600");
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
it("linkifies issue identifiers inside inline code spans", () => {
|
|
|
|
|
const html = renderMarkdown("Reference `PAP-1271` here.", [
|
|
|
|
|
{ identifier: "PAP-1271", status: "done" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(html).toContain('href="/issues/PAP-1271"');
|
|
|
|
|
expect(html).toContain("<code>PAP-1271</code>");
|
|
|
|
|
expect(html).toContain("text-green-600");
|
|
|
|
|
});
|
2026-04-11 06:40:37 -05:00
|
|
|
|
|
|
|
|
it("can opt out of issue reference linkification for offline previews", () => {
|
|
|
|
|
const html = renderToStaticMarkup(
|
|
|
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
|
|
|
<ThemeProvider>
|
|
|
|
|
<MarkdownBody linkIssueReferences={false}>
|
|
|
|
|
{"Depends on PAP-1271 and [manual link](PAP-1271)."}
|
|
|
|
|
</MarkdownBody>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
</QueryClientProvider>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(html).not.toContain('href="/issues/PAP-1271"');
|
|
|
|
|
expect(html).toContain("Depends on PAP-1271");
|
|
|
|
|
expect(html).toContain('href="PAP-1271"');
|
|
|
|
|
});
|
2026-03-19 16:41:28 -05:00
|
|
|
});
|