Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -1410,8 +1410,11 @@
font: var(--maka-text-body);
background: var(--background);
color: var(--foreground);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 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 */
}

Expand Down
41 changes: 41 additions & 0 deletions packages/ui/src/__tests__/markdown-han-script.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<MarkdownBody text="扁平化*只作用于按时间视图*。" density="compact" />);
assert.match(han, /<div[^>]*data-maka-contract="markdown"[^>]*data-maka-script="han"/);

const latin = renderToStaticMarkup(<MarkdownBody text="Flattening *only* applies to the time view." density="compact" />);
assert.doesNotMatch(latin, /data-maka-script/);
});
});
12 changes: 12 additions & 0 deletions packages/ui/src/markdown-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -152,6 +163,7 @@ export function MarkdownBody(props: {
return (
<div
data-maka-contract="markdown"
data-maka-script={hasHanProse(props.text) ? 'han' : undefined}
// Migration-only identity wrapper. `display: contents` gives the
// contract harness a stable declared subtree without adding a layout
// box or interfering with Astryx's document root.
Expand Down
55 changes: 37 additions & 18 deletions packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -409,16 +409,43 @@
[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-list {
gap: var(--md-gap-list);
}
/* 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.
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.
/* 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: headings and fenced code declare their own leading and
keep it, inline code follows the line it sits in, which is what keeps that
line's box even. `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;
}
/* 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 script of the turn's own prose (MarkdownBody
sets `data-maka-script`), not on the document `lang`: that is the UI locale,
and a model answers in whatever language it was asked in. Latin prose keeps
its italic. */
[data-maka-contract="markdown"][data-maka-script="han"] .astryx-markdown em {
font-style: normal;
text-emphasis: filled dot;
text-emphasis-position: under right;
}
/* 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
`<strong>` 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
Expand Down Expand Up @@ -451,14 +478,6 @@
}
[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);
}
/* 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. */
[data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:not([data-level="1"], [data-level="2"], [data-level="3"]) {
color: var(--muted-foreground);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/markdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const TranscriptTurn: Story = {
'',
'#### 边界情况',
'',
'按项目视图仍然保留项目折叠,扁平化只作用于按时间视图。',
'按项目视图仍然保留项目折叠,扁平化*只作用于按时间视图*,按`lastActiveAt`排序,PR #4580 的 3 个 executor 不受影响。',
'',
'> 注意:新增状态只需补一个行图标。',
'',
Expand Down