From 824d3bdb1876f9712464494d51c216bf32176524 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 13:30:10 +0800 Subject: [PATCH 1/4] fix(ui): give CJK prose its own leading, heading rungs and smoothing Chinese transcript text rendered on the same type roles as chrome: 20px leading at 14px, h3 at body size, and body-wide -webkit-font-smoothing: antialiased. Han glyphs fill the em box, so that leading leaves no air between lines; PingFang's Regular and Semibold are close enough that a body-size h3 is indistinguishable from a bold sentence; and antialiased strips the stroke dilation macOS applies, which thins 14px Han glyphs further, most visibly on dark backgrounds. Prose now inherits line-height 1.6 and text-autospace from the markdown root, so headings and code keep their own role leading. The compact heading scale moves one rung up (h1/h2 18px, h3 16px, h4+ body size and muted) so h3 has a size step instead of relying on weight. The smoothing override is dropped in favour of the platform default; it was the only consumer of that declaration. Generated-by: Claude Code --- apps/desktop/src/renderer/maka-tokens.css | 2 -- packages/ui/src/styles.css | 38 +++++++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 03881e213d..dd5f95b8f7 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -1410,8 +1410,6 @@ font: var(--maka-text-body); background: var(--background); color: var(--foreground); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; overflow: hidden; /* main shell handles its own scrolling */ } diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index 659d9415f7..5366b4af0a 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -409,12 +409,23 @@ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-list { gap: var(--md-gap-list); } +/* Prose reads differently from chrome. Han glyphs fill the whole em box, so + the 20px body leading that suits a control row leaves CJK paragraphs with + no air between lines; 1.6 is the common CJK reading ratio. Inherited from + the document root, so headings and code keep the leading their own role + tokens declare. `text-autospace` puts the conventional hair space between + Han and Latin or digit runs when the model did not type one. */ +[data-maka-contract="markdown"] .astryx-markdown { + line-height: 1.6; + text-autospace: normal; +} /* Heading scale: size says "this is a heading", weight and colour say which level. Astryx's document ladder (20/18/16/14) is a page scale and an agent - turn emits `##` every few lines, so the transcript keeps two size steps — - but it keeps TWO, not one. The previous rule flattened h2-h6 onto a single - size, which left h2 and h3 identical in size, weight AND colour: three - levels of structure rendered as three identical bold lines. + turn emits `##` every few lines, so the transcript flattens h1 onto h2 and + keeps two size steps above body: 18 for h1/h2, 16 for h3. h3 needs its own + size rung because weight alone cannot carry it in CJK — PingFang's Regular + and Semibold are close enough that an h3 at body size is indistinguishable + from a bold sentence, and a Chinese turn lost a whole level of structure. Cursor (1.43/1.32/1.21), Claude Code (1.14/1.07/1.0) and Codex (1.71/1.43/1.21) all flatten here relative to their own document styles, and opencode goes further — 17/15/13 with h4+ dropped to a muted colour. Nobody @@ -447,18 +458,19 @@ on its first commit, passing while already false. A comment that is checked when these rules are read beats an assertion that lies. */ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:is([data-level="1"], [data-level="2"]) { - font: var(--maka-text-heading-3); + font: var(--maka-text-heading-2); } -[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:not([data-level="1"], [data-level="2"]) { - font: var(--maka-text-heading-4); +[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading[data-level="3"] { + font: var(--maka-text-heading-3); } -/* Below h3 the size ladder is spent — h4-h6 already sit at body size, and - growing them back would re-create the slabs the flattening exists to avoid. - Colour is the one axis left that costs no vertical space, so the deepest - levels step down in ink instead. opencode makes the same call at the same - point (13px h4-h6 dropped to muted); it is why a turn can carry four levels - of structure inside two type sizes. */ +/* Below h3 the size ladder is spent — h4-h6 sit at body size, and growing + them back would re-create the slabs the flattening exists to avoid. Colour + is the one axis left that costs no vertical space, so the deepest levels + step down in ink instead. opencode makes the same call at the same point + (13px h4-h6 dropped to muted); it is why a turn can carry four levels of + structure inside two type sizes. */ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:not([data-level="1"], [data-level="2"], [data-level="3"]) { + font: var(--maka-text-heading-4); color: var(--muted-foreground); } From 1b228f5788e972cdc8d1bd11c85b054b20502f26 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 13:58:09 +0800 Subject: [PATCH 2/4] fix(ui): mark Chinese emphasis with dots instead of a sheared italic Han faces ship no italic, so `*emphasis*` in a Chinese turn rendered as a synthetic shear of PingFang. Chinese typography marks emphasis with a dot under each character; the rule is keyed on :lang(zh), which LocaleProvider keeps in step with the UI locale, so Latin prose keeps its italic. The transcript story sample gains an emphasis run and a mixed Han/Latin sentence so the case is visible in Storybook. Generated-by: Claude Code --- packages/ui/src/styles.css | 9 +++++++++ packages/ui/stories/markdown.stories.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index 5366b4af0a..e6c7bcd1d5 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -419,6 +419,15 @@ line-height: 1.6; text-autospace: normal; } +/* Han faces have no italic, so `*emphasis*` in Chinese renders as a synthetic + shear of PingFang. Chinese typography marks emphasis with a dot under each + character instead. Keyed on the document language, which LocaleProvider + keeps in step with the UI locale, so Latin prose keeps its italic. */ +[data-maka-contract="markdown"] .astryx-markdown em:lang(zh) { + font-style: normal; + text-emphasis: filled dot; + text-emphasis-position: under right; +} /* Heading scale: size says "this is a heading", weight and colour say which level. Astryx's document ladder (20/18/16/14) is a page scale and an agent turn emits `##` every few lines, so the transcript flattens h1 onto h2 and diff --git a/packages/ui/stories/markdown.stories.tsx b/packages/ui/stories/markdown.stories.tsx index b29b096e51..0a4f3b0c69 100644 --- a/packages/ui/stories/markdown.stories.tsx +++ b/packages/ui/stories/markdown.stories.tsx @@ -88,7 +88,7 @@ export const TranscriptTurn: Story = { '', '#### 边界情况', '', - '按项目视图仍然保留项目折叠,扁平化只作用于按时间视图。', + '按项目视图仍然保留项目折叠,扁平化*只作用于按时间视图*,按`lastActiveAt`排序,PR #4580 的 3 个 executor 不受影响。', '', '> 注意:新增状态只需补一个行图标。', '', From 5d40074593f3f9f9a03a04176c084831545ff79f Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 14:44:41 +0800 Subject: [PATCH 3/4] fix(ui): keep transcript headings at main's sizes, mute h3 instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat-first clients (Claude Code, opencode, Lody) keep h2 within 1.14× of body and h3 at body size, and the transcript scale was flattened for the same reason: a model emits `##` and `###` every few lines, and bigger rungs cut an answer into slabs. So h1/h2 go back to 16px. h3 still cannot be a body-size bold line in CJK — PingFang's Regular and Semibold are too close for it to read as anything but a bold sentence — so it steps down in ink and joins the muted tier with h4-h6 rather than stepping up in size. Generated-by: Claude Code --- packages/ui/src/styles.css | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index e6c7bcd1d5..fe81a8e407 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -428,17 +428,21 @@ text-emphasis: filled dot; text-emphasis-position: under right; } -/* Heading scale: size says "this is a heading", weight and colour say which - level. Astryx's document ladder (20/18/16/14) is a page scale and an agent - turn emits `##` every few lines, so the transcript flattens h1 onto h2 and - keeps two size steps above body: 18 for h1/h2, 16 for h3. h3 needs its own - size rung because weight alone cannot carry it in CJK — PingFang's Regular - and Semibold are close enough that an h3 at body size is indistinguishable - from a bold sentence, and a Chinese turn lost a whole level of structure. - Cursor (1.43/1.32/1.21), Claude Code (1.14/1.07/1.0) and Codex - (1.71/1.43/1.21) all flatten here relative to their own document styles, and - opencode goes further — 17/15/13 with h4+ dropped to a muted colour. Nobody - flattens to zero. Two steps plus colour sits inside that range. +/* Heading scale: size says "this is a heading", colour says which level. + Astryx's document ladder (20/18/16/14) is a page scale and an agent turn + emits `##` and `###` every few lines, so the transcript keeps ONE size step + above body — 16 for h1/h2 — and everything below at body size. Chat-first + clients sit exactly here: Claude Code (1.07/1.0), opencode (15/13 with h4+ + muted), Lody (16/14); only the document-leaning ones (Cursor 1.32/1.21, + Codex 1.43/1.21) go bigger, and a transcript at those sizes reads as a + stack of slabs rather than an answer. + h3 cannot be a body-size bold line in CJK, though: PingFang's Regular and + Semibold are close enough that it is indistinguishable from a bold + sentence, and a Chinese turn lost a whole level of structure. Size would + bring the slabs back, so h3 steps down in ink instead and shares the muted + tier with h4-h6 — the same axis opencode uses one level lower. `####` is + rare in a turn, so h3 and h4 sharing a style costs less than h3 and + `` sharing one. RISK, not just a choice: these are the only rules here that are typography rather than spacing, and Astryx's density RFC (facebook/astryx#839) draws @@ -467,18 +471,9 @@ on its first commit, passing while already false. A comment that is checked when these rules are read beats an assertion that lies. */ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:is([data-level="1"], [data-level="2"]) { - font: var(--maka-text-heading-2); -} -[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading[data-level="3"] { font: var(--maka-text-heading-3); } -/* Below h3 the size ladder is spent — h4-h6 sit at body size, and growing - them back would re-create the slabs the flattening exists to avoid. Colour - is the one axis left that costs no vertical space, so the deepest levels - step down in ink instead. opencode makes the same call at the same point - (13px h4-h6 dropped to muted); it is why a turn can carry four levels of - structure inside two type sizes. */ -[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:not([data-level="1"], [data-level="2"], [data-level="3"]) { +[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:not([data-level="1"], [data-level="2"]) { font: var(--maka-text-heading-4); color: var(--muted-foreground); } From d3cb7180286cac96c3fae182f237cae58d564b57 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 14:56:17 +0800 Subject: [PATCH 4/4] fix(ui): make the smoothing override land and key emphasis on the text's script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #4617 measured that deleting Maka's body smoothing changed nothing: Astryx's reset still sets antialiased on :where(body), so the body role rule now declares auto explicitly. It also showed em:lang(zh) follows the UI locale, which says nothing about what the model wrote — a Chinese interface reading an English answer dotted every letter, and an English interface reading Chinese kept the sheared italic. MarkdownBody now marks its root with data-maka-script="han" when the prose (code excluded) contains Han, and the emphasis rule keys on that instead. Generated-by: Claude Code --- apps/desktop/src/renderer/maka-tokens.css | 5 +++ .../__tests__/markdown-han-script.test.tsx | 41 +++++++++++++++++++ packages/ui/src/markdown-body.tsx | 12 ++++++ packages/ui/src/styles.css | 13 +++--- 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/__tests__/markdown-han-script.test.tsx diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index dd5f95b8f7..3c24b9b58e 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -1410,6 +1410,11 @@ font: var(--maka-text-body); background: var(--background); color: var(--foreground); + /* Astryx's reset puts `antialiased` on `:where(body)`, which strips the + stroke dilation macOS applies and thins 14px Han glyphs. Maka is + CJK-first, so the platform default wins here, for the whole document. */ + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; overflow: hidden; /* main shell handles its own scrolling */ } diff --git a/packages/ui/src/__tests__/markdown-han-script.test.tsx b/packages/ui/src/__tests__/markdown-han-script.test.tsx new file mode 100644 index 0000000000..1f76792d6e --- /dev/null +++ b/packages/ui/src/__tests__/markdown-han-script.test.tsx @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { hasHanProse, MarkdownBody } from '../markdown-body.js'; + +describe('markdown Han-script marker', () => { + it('reads the prose, not the code', () => { + assert.equal(hasHanProse('按 `lastActiveAt` 排序。'), true); + assert.equal(hasHanProse('Sort by `lastActiveAt`.'), false); + assert.equal(hasHanProse('Sort by `排序键`.'), false); + assert.equal(hasHanProse('Example:\n\n```ts\n// 返回值为空时抛错\nreturn null;\n```\n'), false); + assert.equal(hasHanProse('说明:\n\n```ts\nreturn null;\n```\n'), true); + }); + + it('marks the markdown root so the emphasis rule can key on the script of the text', () => { + const han = renderToStaticMarkup(); + assert.match(han, /]*data-maka-contract="markdown"[^>]*data-maka-script="han"/); + + const latin = renderToStaticMarkup(); + assert.doesNotMatch(latin, /data-maka-script/); + }); +}); diff --git a/packages/ui/src/markdown-body.tsx b/packages/ui/src/markdown-body.tsx index e293b4b59e..2f3afdda3e 100644 --- a/packages/ui/src/markdown-body.tsx +++ b/packages/ui/src/markdown-body.tsx @@ -111,6 +111,17 @@ export function applyMermaidRenderBudget(source: string): string { return lines.join('\n'); } +// Whether the prose (not code) of a markdown source is written in Han script. +// The document `lang` is the UI locale, which says nothing about what the +// model wrote, so the CSS that styles Han-specific runs (emphasis has no +// italic in Han faces) keys on this instead. +export function hasHanProse(source: string): boolean { + const prose = source + .replace(/^( {0,3})(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n {0,3}\2[ \t]*$/gm, '') + .replace(/`[^`\n]*`/g, ''); + return /\p{Script=Han}/u.test(prose); +} + const MARKDOWN_COMPONENTS = { default: { ...BASE_MARKDOWN_COMPONENTS, @@ -152,6 +163,7 @@ export function MarkdownBody(props: { return (