[codex] Fix markdown contrast on accent bubbles (#8689)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The issue/task chat subsystem renders board and agent comments as markdown inside chat bubbles. > - Human board comments use a saturated blue bubble so the message can be distinguished from agent output. > - The shared `.paperclip-markdown` rule forced `var(--foreground)` for body text and related markdown elements. > - On the blue bubble, that foreground color is too dark, so paragraphs, lists, links, and emphasis lose contrast. > - This pull request adds an explicit on-accent markdown treatment for blue/current-user message bubbles. > - The benefit is readable markdown without changing neutral markdown surfaces elsewhere in the app. ## Linked Issues or Issue Description Fixes #8688 ## What Changed - Added a `paperclip-markdown-on-accent` class for markdown rendered on accent-colored message bubbles. - Updated current-user issue chat messages to use the accent-safe markdown class. - Added CSS overrides for body text, list markers, headings, emphasis, links, blockquotes, and inline code on accent backgrounds. - Added a regression test proving current-user blue-bubble markdown receives the accent-safe class. ## Before Screenshot: <img width="816" height="618" alt="Screenshot 2026-06-27 at 14 22 59" src="https://github.com/user-attachments/assets/2a982326-9ff3-40ce-9955-ceb081d8a2e0" /> ## After Screenshot: <img width="821" height="624" alt="Screenshot 2026-06-27 at 15 05 17" src="https://github.com/user-attachments/assets/4722a0db-9f6a-4775-a7eb-47328c2f71e0" /> ## Verification - `CI=true corepack pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx` - `CI=true corepack pnpm --filter @paperclipai/ui typecheck` - GitHub Actions PR workflow passed on latest head `f645cca3fc9675071c201471c0df938880c0c351`, including Build, Typecheck + Release Registry, e2e, general test shards, serialized server shards, Canary Dry Run, and aggregate verify. - Greptile Review passed with Confidence Score 5/5 on latest head `f645cca3fc9675071c201471c0df938880c0c351`. ## Risks Low risk. The new color treatment is opt-in and only applied to current-user issue chat markdown on accent bubbles. Neutral comment, document, artifact, and skill markdown surfaces keep the existing `.paperclip-markdown` colors. > 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 GPT-5 Codex in Codex desktop, with repository tool use and local command execution. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
a7a73d5bc7
commit
4a2447da3c
|
|
@ -90,9 +90,9 @@ vi.mock("../lib/issue-chat-scroll", async (importOriginal) => {
|
|||
});
|
||||
|
||||
vi.mock("./MarkdownBody", () => ({
|
||||
MarkdownBody: ({ children }: { children: ReactNode }) => {
|
||||
markdownBodyRenderMock(children);
|
||||
return <div>{children}</div>;
|
||||
MarkdownBody: ({ children, className }: { children: ReactNode; className?: string }) => {
|
||||
markdownBodyRenderMock({ children, className });
|
||||
return <div className={className}>{children}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -354,6 +354,48 @@ describe("IssueChatThread", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("uses accent-safe markdown color in the current user's blue message bubble", () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<MemoryRouter>
|
||||
<IssueChatThread
|
||||
comments={[{
|
||||
id: "comment-current-user",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
authorAgentId: null,
|
||||
authorUserId: "user-board",
|
||||
authorType: "user",
|
||||
body: "1. **Readable** markdown on blue",
|
||||
presentation: null,
|
||||
metadata: null,
|
||||
createdAt: new Date("2026-04-06T12:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-06T12:00:00.000Z"),
|
||||
}]}
|
||||
linkedRuns={[]}
|
||||
timelineEvents={[]}
|
||||
liveRuns={[]}
|
||||
currentUserId="user-board"
|
||||
onAdd={async () => {}}
|
||||
showComposer={false}
|
||||
enableLiveTranscriptPolling={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(markdownBodyRenderMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
children: "1. **Readable** markdown on blue",
|
||||
className: expect.stringContaining("paperclip-markdown-on-accent"),
|
||||
}));
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("labels operator-interrupted cancelled runs as interrupted while preserving plain cancelled runs", () => {
|
||||
const root = createRoot(container);
|
||||
const linkedRuns: IssueChatLinkedRun[] = [
|
||||
|
|
|
|||
|
|
@ -721,9 +721,7 @@ const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed, onAc
|
|||
}
|
||||
return (
|
||||
<WorkspaceFileMarkdownBody
|
||||
// `prose-invert` flips typography colours (body, links, code) to light so
|
||||
// markdown stays legible on the saturated liveness-blue human bubble.
|
||||
className={cn("text-sm leading-6", onAccent && "prose-invert")}
|
||||
className={cn("text-sm leading-6", onAccent && "paperclip-markdown-on-accent")}
|
||||
style={recessed ? { opacity: 0.55 } : undefined}
|
||||
softBreaks
|
||||
onImageClick={onImageClick}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const stylesheet = readFileSync(fileURLToPath(new URL("../index.css", import.meta.url)), "utf8");
|
||||
|
||||
function cssBlock(selector: string): string {
|
||||
const start = stylesheet.indexOf(`${selector} {`);
|
||||
expect(start, `Missing CSS selector: ${selector}`).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const bodyStart = stylesheet.indexOf("{", start);
|
||||
const bodyEnd = stylesheet.indexOf("\n}", bodyStart);
|
||||
expect(bodyStart, `Missing CSS block start: ${selector}`).toBeGreaterThanOrEqual(0);
|
||||
expect(bodyEnd, `Missing CSS block end: ${selector}`).toBeGreaterThan(bodyStart);
|
||||
|
||||
return stylesheet.slice(bodyStart + 1, bodyEnd);
|
||||
}
|
||||
|
||||
describe("accent markdown styles", () => {
|
||||
it("inherits the current message bubble foreground for prose text, links, and list counters", () => {
|
||||
const block = cssBlock(".paperclip-markdown.paperclip-markdown-on-accent");
|
||||
|
||||
expect(block).toContain("color: inherit");
|
||||
expect(block).toContain("--tw-prose-body: currentColor");
|
||||
expect(block).toContain("--tw-prose-links: currentColor");
|
||||
expect(block).toContain("--tw-prose-counters: currentColor");
|
||||
expect(block).toContain("--tw-prose-bullets: currentColor");
|
||||
expect(block).toContain("--tw-prose-invert-links: currentColor");
|
||||
});
|
||||
|
||||
it("keeps ordered-list markers and rendered link variants readable on accent bubbles", () => {
|
||||
expect(cssBlock(".paperclip-markdown.paperclip-markdown-on-accent li::marker")).toContain(
|
||||
"color: currentColor",
|
||||
);
|
||||
expect(cssBlock(".paperclip-markdown.paperclip-markdown-on-accent :where(a, a:visited)")).toContain(
|
||||
"color: currentColor",
|
||||
);
|
||||
expect(cssBlock(".paperclip-markdown.paperclip-markdown-on-accent .paperclip-workspace-file-link")).toContain(
|
||||
"color: currentColor",
|
||||
);
|
||||
expect(
|
||||
cssBlock(
|
||||
".paperclip-markdown.paperclip-markdown-on-accent :where(a.paperclip-mention-chip, a.paperclip-project-mention-chip)",
|
||||
),
|
||||
).toContain("color: currentColor !important");
|
||||
});
|
||||
});
|
||||
|
|
@ -1042,12 +1042,49 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
background-color: #ffffff0f;
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent :not(pre) > code {
|
||||
background-color: color-mix(in oklab, currentColor 24%, transparent);
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.paperclip-markdown {
|
||||
color: var(--foreground);
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent {
|
||||
color: inherit;
|
||||
--tw-prose-body: currentColor;
|
||||
--tw-prose-headings: currentColor;
|
||||
--tw-prose-lead: currentColor;
|
||||
--tw-prose-links: currentColor;
|
||||
--tw-prose-bold: currentColor;
|
||||
--tw-prose-counters: currentColor;
|
||||
--tw-prose-bullets: currentColor;
|
||||
--tw-prose-hr: color-mix(in oklab, currentColor 24%, transparent);
|
||||
--tw-prose-quotes: currentColor;
|
||||
--tw-prose-quote-borders: color-mix(in oklab, currentColor 56%, transparent);
|
||||
--tw-prose-captions: color-mix(in oklab, currentColor 84%, transparent);
|
||||
--tw-prose-code: currentColor;
|
||||
--tw-prose-th-borders: color-mix(in oklab, currentColor 34%, transparent);
|
||||
--tw-prose-td-borders: color-mix(in oklab, currentColor 22%, transparent);
|
||||
--tw-prose-invert-body: currentColor;
|
||||
--tw-prose-invert-headings: currentColor;
|
||||
--tw-prose-invert-lead: currentColor;
|
||||
--tw-prose-invert-links: currentColor;
|
||||
--tw-prose-invert-bold: currentColor;
|
||||
--tw-prose-invert-counters: currentColor;
|
||||
--tw-prose-invert-bullets: currentColor;
|
||||
--tw-prose-invert-hr: color-mix(in oklab, currentColor 24%, transparent);
|
||||
--tw-prose-invert-quotes: currentColor;
|
||||
--tw-prose-invert-quote-borders: color-mix(in oklab, currentColor 56%, transparent);
|
||||
--tw-prose-invert-captions: color-mix(in oklab, currentColor 84%, transparent);
|
||||
--tw-prose-invert-code: currentColor;
|
||||
--tw-prose-invert-th-borders: color-mix(in oklab, currentColor 34%, transparent);
|
||||
--tw-prose-invert-td-borders: color-mix(in oklab, currentColor 22%, transparent);
|
||||
}
|
||||
|
||||
.paperclip-markdown > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -1087,6 +1124,10 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent li::marker {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.paperclip-markdown h1,
|
||||
.paperclip-markdown h2,
|
||||
.paperclip-markdown h3,
|
||||
|
|
@ -1115,6 +1156,10 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent :where(h1, h2, h3, h4, strong, b) {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.paperclip-markdown :where(strong, b) {
|
||||
color: var(--foreground);
|
||||
font-weight: 600;
|
||||
|
|
@ -1127,6 +1172,16 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent :where(a, a:visited) {
|
||||
color: currentColor;
|
||||
text-decoration-color: color-mix(in oklab, currentColor 72%, transparent);
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent :where(a:hover, a:focus-visible) {
|
||||
color: currentColor;
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.paperclip-markdown a.paperclip-mention-chip {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
@ -1154,6 +1209,10 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
color: color-mix(in oklab, var(--foreground) 80%, #58a6ff 20%);
|
||||
}
|
||||
|
||||
.dark .paperclip-markdown.paperclip-markdown-on-accent :where(a, a:visited) {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.paperclip-markdown blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 0.95rem;
|
||||
|
|
@ -1161,6 +1220,27 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent blockquote {
|
||||
border-left-color: color-mix(in oklab, currentColor 56%, transparent);
|
||||
color: color-mix(in oklab, currentColor 88%, transparent);
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent .paperclip-workspace-file-link {
|
||||
border-color: color-mix(in oklab, currentColor 34%, transparent);
|
||||
background-color: color-mix(in oklab, currentColor 16%, transparent);
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent .paperclip-workspace-file-link:hover {
|
||||
background-color: color-mix(in oklab, currentColor 24%, transparent);
|
||||
}
|
||||
|
||||
.paperclip-markdown.paperclip-markdown-on-accent :where(a.paperclip-mention-chip, a.paperclip-project-mention-chip) {
|
||||
border-color: color-mix(in oklab, currentColor 34%, transparent) !important;
|
||||
background: color-mix(in oklab, currentColor 16%, transparent) !important;
|
||||
color: currentColor !important;
|
||||
}
|
||||
|
||||
.paperclip-markdown hr {
|
||||
margin: 1.25rem 0;
|
||||
border-color: var(--border);
|
||||
|
|
|
|||
Loading…
Reference in New Issue