fix(ui): space CJK-Latin mixed prose in Markdown answers - #1482
Merged
Merged
Conversation
Maka is CJK-first and its assistant prose is dense mixed script — Chinese wrapped around identifiers, paths, version numbers and line numbers. Four spacing gaps showed up when measuring that surface against the built app. - `text-autospace: normal` on `.maka-prose`. Chromium's initial value is `no-autospace`, not the spec's `normal`, so the CJK/Latin boundary spacing has to be asked for. Measured +1.55px per boundary at the 13px body (1/8 em), and it reaches across inline element boundaries, so `<code>` pills get it on both sides too. Scoped to the prose layer rather than `:root`: the composer, sidebar and settings surfaces would all shift text metrics, which is a separate call. - `margin-inline: 0.25em` on inline code. Autospace alone leaves 1.55px against a pill that already carries padding and a filled background, where the code still reads glued to the surrounding Chinese. The two stack rather than replace each other. - `word-break: auto-phrase` on `td`. A squeezed Chinese body cell otherwise breaks mid-word. Only `td` — `th` is `white-space: nowrap`, where a break rule is inert. - `.maka-hardbreak` for hard breaks. `remark-breaks` turns every single newline into a hard break, and model answers lean on `**subhead**\nbody` to separate sections, so this gap routinely carries a section split. It had only the 4px the line box already gives, against 16px between paragraphs — the stronger semantic split rendered tighter than the weaker one. CSS cannot space a native `<br>` (display:block, height, margin and content:"\A" each leave the paragraph at exactly 39px), so markdown-body renders it as a block span that can take a height. The gap lands at 10px, between the 4px line gap and the 16px paragraph gap. Every number here comes from the built Electron app via CDP, not headless: headless Chromium silently falls PingFang SC back to a serif face while `document.fonts.check` still reports true, which invalidates any font-dependent measurement taken there. Rejected while measuring, each for a recorded reason: narrowing the reading measure (Paseo, opencode and LobeChat all have no fixed measure at all); `letter-spacing: 0.02em` (conflicts with the CJK-first no-tracking decision in prose.css); `font-synthesis: style` (PingFang SC has a real Semibold, so the three settings render identically); a CJK font in the mono stack (code-block Chinese already falls back to PingFang SC and Geist Mono resolves correctly); `remark-cjk-friendly` (10 emphasis cases pass on the current remark; only intraword `_` fails, which is CommonMark's design and applies to Latin too); and `text-box-trim` (LobeChat shipped, narrowed and reverted it within a week). MARKDOWN-PROSE-CJK-MIXED-SPACING-0 locks all four.
…ert rule Three-way review of #1482 (Codex / independent agent / Kimi K3) converged on the same defect class, reproduced here in the built app (Electron 43.1.1, Chromium 150): - `margin-inline: 0.25em` on `.maka-prose code` (0,1,1) also matched `pre > code`. A <code> inside `white-space: pre` is ONE inline box spanning every line, so the margin offset only the FIRST source line of every fenced block: measured 36.25px against 33px for the lines below it. - `text-autospace: normal` is inherited, so it reached fenced code too and drifted CJK/Latin lines ~1.6px off the monospace grid per boundary. Both are the invariant #1431 already locked for ligatures — code is a literal surface — reached a second time by different properties, so the fix is the boundary rather than the two leaks: the `.maka-code-block pre` tier now carves prose shaping back out, and the contract fails when ANY future shaping declaration lands on `.maka-prose` without a carve-out (verified by adding a letter-spacing declaration and watching it go red). Also: - Drop `td { word-break: auto-phrase }`. Measured inert for Chinese in this engine: identical break positions to `normal` under `lang="zh-CN"` and `lang="en"`; only `lang="ja"` changes, since Chromium's phrase model is Japanese-only. Double-gated by the renderer's static `<html lang="en">`. The rule, its comment and its assertion all claimed a behaviour that does not occur. - Emit `<br>` + the spacing span instead of replacing the `<br>`. Measured with CDP: the bare span dropped the `LineBreak` node from the accessibility tree. Both elements keep the AX node, keep Selection.toString() at a single "\n", and still take the paragraph to 45px. - Move the DOM half of the contract into packages/ui's render test, where AST → DOM belongs. It asserts on a single-newline source, so removing `remarkBreaks` now fails the suite instead of silently making `.maka-hardbreak` dead CSS.
The rules' rationale was written out three or four times across prose.css, markdown-body.tsx and both test files — measurement methodology, rejected alternatives and the full <br> negative result each restated per site. Keep the reason and the number at the rule; drop the retelling. 188 added lines instead of 275, 48 comment lines instead of 112.
…ean something Round-2 review (Codex, an independent agent, z.ai glm-5.2) found three more instances of one shape: a rule whose scope is wider than its intent. All measured in the shipping runtime. - `margin-inline` on the inline pill is the gap between the pill and its NEIGHBOURING text — at a block edge there is no neighbour, so it indented the block instead. A paragraph, list item or blockquote opening with inline code sat 2.98px right of one opening with plain text: a ragged left edge down a list of such items. Zeroed at both edges. - `.maka-hardbreak`'s 6px is for the section split LLM answers write as `**subhead**\nbody`, but `remark-breaks` makes EVERY newline a break, so it also landed on continuation lines inside list items and blockquotes — where it inverted the grouping: 10.5px inside one list item against 6.5px between two of them. Those containers already group their own content, so the gap is dropped there and kept in paragraphs. - `border-radius` reached fenced code through the same `.maka-prose code` selector as `margin`; `border: 0` does not clear it. Zeroed in the reset. The boundary contract missed the first two because it was a presence check wearing a neutralisation check's error message: it asserted the property NAME reappeared in the code-block tier, never the value. Reverting a carve-out to the leaking value kept it green — verified by mutation, twice. It now asserts the neutral value, scans every rule that can contain a fenced block (not just the bare `.maka-prose` one, and not `h1`/`th`, which cannot hold code), and covers the two new invariants. All four mutations that previously shipped a regression green now fail. Three comment facts corrected while here: the renderer does NOT pin `lang="en"` (syncUiLocaleDocument sets it at runtime — the auto-phrase removal stands on the Japanese-only phrase model alone), the `<br>` buys the AX LineBreak node and nothing else (the span carries all 6px and copy fidelity is identical either way), and the pill rule got its measured numbers back.
orangeCatDeveloper
added a commit
to orangeCatDeveloper/maka
that referenced
this pull request
Sep 7, 2026
The desktop dist suite hung its CI job on run 34061414876 with every reported test passing: the vite-workspace-packages test file completed both subtests, but its node:test child process stayed alive for 811s until the workspace runner hit the 900s ceiling and killed the run. The lingering handle is a chokidar fs.watch on the temporary repository root, orphaned by a TOCTOU race between chokidar close() and the in-flight scan of the dev-server restart that the first test triggers (apache#4940 has the full fs-level attribution). The second test dodged this class in apache#4748 by dropping its watcher; the first test needs its watcher to assert restart-on-manifest-change, so it cannot. --test-force-exit makes the runner exit once every test has finished, so completed tests still report their real results while post-test lingering handles can no longer hold the workspace run hostage. Fixes apache#4940 Generated-by: GLM-5.3-Flash (pi) The "//test:dist" key documents the removal condition next to the flag: drop it once the chokidar closer-drop fix ships upstream (apache#1482) and reaches vite, tracked in apache#4940.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Maka is CJK-first and its assistant prose is dense mixed script — Chinese wrapped around identifiers, paths, version numbers and line numbers. Measuring that surface against the built app turned up three spacing gaps, plus a literal-code boundary that has to hold for them. Refs #1433 (item 5, reframed).
text-autospace: normalon.maka-prose. Chromium's initial value isno-autospace, not the spec'snormal, so the CJK/Latin boundary spacing has to be asked for. It reaches across inline element boundaries, so<code>pills get it on both sides too. Scoped to the prose layer rather than:root: the composer, sidebar and settings surfaces would all shift text metrics, which is a separate call.margin-inline: 0.25emon inline code. Autospace alone leaves 1.55px against a pill that already carries padding and a filled background, where the code still reads glued to the surrounding Chinese. The two stack rather than replace each other — measured 78.36px → 81.48px (autospace) → 87.45px (both) on the same paragraph. Zeroed at block edges: that margin is the gap to neighbouring text, and at a block edge there is none, so it indented the block instead (2.98px, a ragged left edge down a list of items opening with code).<br>+.maka-hardbreakfor hard breaks.remark-breaksturns every single newline into a hard break, and model answers lean on**subhead**\nbodyto separate sections, so this gap routinely carries a section split. It had only the 4px the line box already gives, against 16px between paragraphs — the stronger semantic split rendered tighter than the weaker one. CSS cannot space a native<br>:display:block,height,marginandcontent:"\A"each leave the paragraph at exactly 39px, because an inline break box cannot take a height. Somarkdown-bodyemits the native<br>followed by a block span. Both are needed: the span alone drops theLineBreaknode from the accessibility tree (verified via CDP), and the<br>alone cannot be given a height. Together they keep the AX node, keepSelection.toString()at a single\n, and land the gap at 10px between the 4px line gap and the 16px paragraph gap.The hard-break gap is scoped to paragraphs.
remark-breaksmakes every newline a break, so the 6px also landed on continuation lines inside list items and blockquotes, inverting the grouping: 10.5px inside one list item against 6.5px between two of them. Those containers already group their own content.The literal-code boundary. The first two rules are prose decoration, and both reach fenced code unless carved back out —
text-autospaceinherits, and.maka-prose code(0,1,1) matchespre > codeas well as the inline pill. This is the invariant #1431 already locked for ligatures ("code is a literal surface"), reached a second time by different properties, so.maka-code-block prenow carves prose shaping out as a named boundary and the contract guards the boundary rather than the two known leaks.MARKDOWN-PROSE-CJK-MIXED-SPACING-0locks the CSS tier;packages/ui's render test locks the DOM tier.Verification
npm --workspace @maka/desktop run test:dist2850/2850 ·npm --workspace @maka/ui run test·npm run typecheck·npm run lint·npm run format:check·node scripts/check-dead-css.mjs.Measured in the built Electron app (Electron 43.1.1 / Chromium 150), not headless. That distinction is load-bearing: headless Chromium silently falls PingFang SC back to a serif face while
document.fonts.checkstill reportstrue, so every font-dependent number taken there is void. Confirmed withCSS.getPlatformFontsForNodethat the real stack resolves toPingFang SC/.SF NS/Geist Mono (custom).The contract asserts the neutral value of each carve-out, not just that the property name reappears — the first version was a presence check wearing a neutralisation check's error message, and reverting a carve-out to the leaking value kept it green. Deletion-sensitivity was checked by breaking it four ways and confirming each goes red: removing the
preautospace carve-out, removing thepre codemargin reset, and adding a freshletter-spacingto.maka-prosewith no carve-out. RemovingremarkBreaksnow failspackages/ui's render test, which previously would have left.maka-hardbreakas unreachable dead CSS with every assertion still green.Review focus
Three-way review (Codex, an independent agent, Kimi K3 256K) all returned FAIL on the first revision and converged on the same defect class; the second commit is the response. What they found, and what changed:
<code>inwhite-space: preis one inline box across all lines, so only line 1 moves.)word-break: auto-phraseontdwas inert. Removed. Chromium's phrase model is Japanese-only: identical break positions tonormalunderlang="zh-CN"andlang="en", whilelang="ja"does change. Double-gated by the renderer's static<html lang="en">. The rule, its comment and its test all asserted a behaviour that does not occur.LineBreaknode. Fixed by emitting<br>+ span. Nesting the<br>inside the span instead is what produces a spurious"\n\n"on copy; the sibling form has no such cost.packages/ui's render test where AST → DOM belongs, and the CSS half now guards the boundary as a property class.An E2E computed-style spec was suggested by all three and not taken:
MAKA_E2E_FIXTUREis a scenario enum, not free-text injection, so pinning specific Markdown would mean adding a product-layer fixture scenario — disproportionate here. The two seams used instead are a real render (packages/ui) and a boundary-completeness contract.Still open as a judgement call:
text-autospacestays on.maka-prose, so a user's own message bubble does not get the same treatment as the assistant's answer. LobeChat puts it on:root. Widening it is a follow-up, not an oversight.Several things were rejected while measuring, each for a recorded reason: narrowing the reading measure (Paseo, opencode and LobeChat all have no fixed measure at all — the premise in #1433 item 5 does not hold);
letter-spacing: 0.02em(conflicts with the CJK-first no-tracking decision already inprose.css);font-synthesis: style(PingFang SC has a real Semibold, so all three settings render identically); a CJK font in the mono stack (code-block Chinese already falls back to PingFang SC, and Geist Mono resolves correctly);remark-cjk-friendly(10 emphasis cases pass on the current remark — only intraword_fails, which is CommonMark's design and applies to Latin equally); andtext-box-trim(LobeChat shipped, narrowed and reverted it inside a week).