Commit Graph
2 Commits
Author SHA1 Message Date
scotttongandClaude Opus 5 27897e754e feat(markdown): add syntax colors to rendered code blocks
The block follows the theme now, but every token in it is one color.
Add syntax highlighting, using the palette from the approved design.

Render: add rehype-highlight and pass it to react-markdown. The plugin
array sits at module scope, because react-markdown remounts the rendered
tree when the array identity changes. That is the same hazard the
remarkPlugins memo already guards against.

Detection: `detect` is on, because most agent-authored fences carry no
language tag and would never highlight without it. `subset` restricts
detection to the languages agents actually post. highlight.js guesses
badly on short snippets, and a wrong guess colors the whole block wrongly.

Palette: eight `--code-syntax-*` aliases carry the roles. Each one points
at a token that already exists and already has a `.dark` override, so a
custom property resolves per mode at its use site and one definition
covers both themes.

The mapping is BORROWED, and this is the part to review. Paperclip has no
syntax palette. `--status-task-icon-*` and `--chip-match-title-fg` name
task-status and search-chip concerns. They were used because they are the
only hues that carry a real per-mode value; the fixed `--status-task-*`
hues go dim on the dark code surface. The coupling is confined to the
alias block. Repointing those eight lines moves the whole palette, and no
rule changes. A test asserts that the syntax rules never reference the
borrowed tokens directly.

Verified in both themes on a real task. Light resolves keyword #7c3aed,
identifier oklch(0.145 0 0), function oklch(0.4 0.13 265), comment
oklch(0.556 0 0). Dark resolves #9474f0, oklch(0.985 0 0),
oklch(0.78 0.1 265), oklch(0.708 0 0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:49:57 -07:00
scotttongandClaude Opus 5 a1278e6ded fix(markdown): make rendered code blocks follow the active theme (#11591)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents report their work in task chat, and that chat renders
markdown through `MarkdownBody`
> - Agents post shell commands, API payloads and diffs, so a fenced code
block is one of the most frequent things a person reads in the product
> - The rendered code block was pinned to two literal colors, so it
stayed dark in light mode and was the only dark surface on a light page
> - The `prose` and the `prose-invert` variables held the same two
values, so the theme could not change the block at all
> - This pull request binds the block to the theme tokens that already
exist and already carry a `.dark` override
> - The benefit is that a code block now matches the page in both modes,
and it follows any future change to the theme automatically

## Linked Issues or Issue Description

No public issue exists. The problem is described below.

**What happened?**
A fenced code block in rendered markdown is always dark. It uses the
background `#1e1e2e` and the text color `#cdd6f4` in light mode and in
dark mode. In light mode the block is the only dark surface on the page.
The rule is in `ui/src/index.css`. The variables `--tw-prose-pre-bg` and
`--tw-prose-invert-pre-bg` are set to the same literal value, so
`prose-invert` cannot change it.

**Expected behavior**
A code block uses a light surface with dark text in light mode. It uses
a dark surface with light text in dark mode. It follows the theme like
every other surface in the app.

**Steps to reproduce**
1. Start the app and open a task that contains a fenced code block in
its chat.
2. Set the theme to light.
3. Look at the code block. The block is dark. The page is light.
4. Set the theme to dark. The block does not change.

**Paperclip version or commit**
Reproduced on `master` at `4af55ba6b`.

**Agent adapter(s) involved**
Not adapter-specific (core bug). The defect is in the UI render path.

**Deployment mode**
Local development server.

## What Changed

- Bind `.paperclip-markdown pre` to `--muted`, `--foreground`,
`--border` and `--radius-lg` in `ui/src/index.css`. Remove the `#1e1e2e`
and `#cdd6f4` literals.
- Set the four `--tw-prose-*-pre-*` variables to the same tokens, so
`prose` and `prose-invert` both follow the theme.
- Apply the same four tokens to `.paperclip-mdxeditor-content pre`.
- Change the fill of the copy button and the wrap button to
`--background` in `ui/src/components/MarkdownBody.tsx`. The old fill was
`color-mix(in oklab, var(--muted) 92%, var(--background) 8%)`. That
value is almost equal to the block's new `--muted` surface, so the
buttons would nearly disappear.
- Update the stale comment above the rule. The comment said "Dark theme
code blocks".
- Add `ui/src/components/MarkdownCodeBlockStyles.test.ts`. It fails if a
literal color returns to any themed code surface.
- Export `codeBlockActionStyle` from `MarkdownBody.tsx` so the new test
can read it.

No new design token is added. Every token used here is already defined
in `ui/src/index.css`, and each one already has a `.dark` override.

## Verification

Automated:

```bash
cd ui && npx vitest run --config vitest.config.ts src/components/MarkdownCodeBlockStyles.test.ts src/components/MarkdownBody.test.tsx src/components/MarkdownBody.wrap.test.tsx src/components/MarkdownAccentStyles.test.ts
```

59 tests pass.

`MarkdownCodeBlockStyles.test.ts` is new. It guards the defect directly.
It reads `index.css` and asserts that each themed code surface rides a
token and holds no hex, `rgb()` or `hsl()` literal. I confirmed the test
fails on the original defect: restoring `#1e1e2e` and `#cdd6f4` fails 2
of the 5 tests.

Manual. Open a task that has a fenced code block in its chat. Read the
computed style of `.paperclip-markdown pre` in the browser. The measured
values are:

| Property | Light | Dark |
| --- | --- | --- |
| background | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` |
| color | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` |
| border | `oklch(0.922 0 0)` | `oklch(1 0 0 / 0.1)` |
| radius | `8px` | `8px` |

These values are `--muted`, `--foreground`, `--border` and `--radius-lg`
for each mode. Toggle the theme and confirm the block changes with the
page.

## Risks

Low risk. The change is CSS and one inline style value. There is no
migration and no schema change.

Two behavioral notes for the reviewer:

1. The corner radius changes from `calc(var(--radius) - 3px)` (5px) to
`var(--radius-lg)` (8px). This matches the approved design and removes
an arbitrary offset. It is a small visual change on every code block.
2. The CodeMirror theme inside the MDXEditor still uses the Catppuccin
literals (`ui/src/index.css`, the `.paperclip-mdxeditor .cm-editor`
rules). That surface is an editor, not a rendered snippet, and a change
there needs a full light CodeMirror theme. A code block therefore looks
light when it is rendered and dark while a person edits it. This is
intentional in this pull request. Tell me if you want it in scope.

Syntax highlighting, line numbers and diff rows are not in this pull
request. The repository has no highlighter dependency and no syntax or
diff tokens. Those are separate changes.

## Model Used

Claude Opus 5 (`claude-opus-5`), via Claude Code. Extended thinking was
on. Tool use was on, with file edit, shell, browser automation and the
Paper design tool. The design was produced first in Paper, then read
back through the design tool for exact token values rather than from
screenshots.

## 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)
- [ ] 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Notes on the unchecked boxes:

- Branch name. The branch is
`claude/paperclip-code-snippet-styling-202f7d`. It describes the change,
but the `202f7d` suffix comes from the local worktree name. It carries
no ticket id. I did not rename it, because the branch was already named
when this work was requested. Tell me if you want it renamed before
review.
- CI and Greptile. Not yet run at the time of opening.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:36:56 -07:00