From 4a2447da3cec369dcac67ad3e68c4e17f7684c15 Mon Sep 17 00:00:00 2001 From: Aron Prins Date: Mon, 29 Jun 2026 22:24:33 +0200 Subject: [PATCH] [codex] Fix markdown contrast on accent bubbles (#8689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: Screenshot 2026-06-27 at 14 22 59 ## After Screenshot: Screenshot 2026-06-27 at 15 05 17 ## 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 --- ui/src/components/IssueChatThread.test.tsx | 48 ++++++++++- ui/src/components/IssueChatThread.tsx | 4 +- .../components/MarkdownAccentStyles.test.ts | 47 +++++++++++ ui/src/index.css | 80 +++++++++++++++++++ 4 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 ui/src/components/MarkdownAccentStyles.test.ts diff --git a/ui/src/components/IssueChatThread.test.tsx b/ui/src/components/IssueChatThread.test.tsx index 8be26a9f74..2625ecaa49 100644 --- a/ui/src/components/IssueChatThread.test.tsx +++ b/ui/src/components/IssueChatThread.test.tsx @@ -90,9 +90,9 @@ vi.mock("../lib/issue-chat-scroll", async (importOriginal) => { }); vi.mock("./MarkdownBody", () => ({ - MarkdownBody: ({ children }: { children: ReactNode }) => { - markdownBodyRenderMock(children); - return
{children}
; + MarkdownBody: ({ children, className }: { children: ReactNode; className?: string }) => { + markdownBodyRenderMock({ children, className }); + return
{children}
; }, })); @@ -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( + + {}} + showComposer={false} + enableLiveTranscriptPolling={false} + /> + , + ); + }); + + 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[] = [ diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index ec3534f4c0..3aafbf53be 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -721,9 +721,7 @@ const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed, onAc } return ( { + 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"); + }); +}); diff --git a/ui/src/index.css b/ui/src/index.css index d749022899..f86dd53ba9 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -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);