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
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Deep-thinking body wrap contract.
*
* Astryx's ejected ChatReasoning (packages/ui/src/astryx-chat-reasoning.tsx)
* owns no white-space on its content shell — the component assumes children
* are pre-rendered content, and its StyleX atoms declare nothing, so the
* inherited `white-space: normal` collapses every newline in the thinking
* text ("深度思考换行被吞"). The product restores the reading contract with a
* product class on the reasoning body + one CSS rule in @maka/ui styles.css.
*
* This pins the CSS half of that seam: the final effective cascade value of
* `white-space`/`word-break` for `.maka-chat-reasoning-content` must be
* pre-wrap/break-word. Within the components layer, the last rule declaring a
* property wins, so the assertion walks every matching rule body in source
* order and checks the last declaration of each property — a later rule that
* re-declares `white-space: normal` fails here even while an earlier rule
* still says pre-wrap, and a harmless addition (a focus outline, a media
* variant that leaves white-space alone) stays green. The renderer half (the
* class actually landing on the content div) is locked by the deep-thinking
* disclosure test in packages/ui/src/__tests__/processing-block.test.tsx.
*/
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/** Every rule body whose selector matches `selector`, in source order. */
function cssRuleBodies(css: string, selector: string): string[] {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`(?:^|[\\{\\}])\\s*${escaped}\\s*\\{`, 'g');
const bodies: string[] = [];
let match: RegExpExecArray | null;
while ((match = re.exec(css)) !== null) {
const open = match.index + match[0].length - 1;
let depth = 1;
let i = open + 1;
while (i < css.length && depth > 0) {
if (css[i] === '{') depth += 1;
else if (css[i] === '}') depth -= 1;
i += 1;
}
bodies.push(css.slice(open + 1, i - 1));
}
return bodies;
}

/** Last value declared for `prop` across all bodies, in cascade (source) order. */
function lastEffective(bodies: string[], prop: string): string | undefined {
let value: string | undefined;
for (const body of bodies) {
for (const match of body.matchAll(new RegExp(`${prop}\\s*:\\s*([^;}]+)`, 'g'))) {
value = match[1]!.trim();
}
}
return value;
}

describe('deep-thinking body wrap contract', () => {
it('renders the reasoning body with pre-wrap, as the final effective declaration', async () => {
const css = stripCssComments(await readAllRendererCss());
const bodies = cssRuleBodies(css, '.maka-chat-reasoning-content');

assert.ok(bodies.length > 0, '.maka-chat-reasoning-content rule must exist');
assert.equal(
lastEffective(bodies, 'white-space'),
'pre-wrap',
'the final effective white-space for the reasoning body must be pre-wrap',
);
assert.equal(
lastEffective(bodies, 'word-break'),
'break-word',
'the final effective word-break for the reasoning body must be break-word',
);
});
});
5 changes: 5 additions & 0 deletions packages/ui/src/__tests__/processing-block.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ describe('deep-thinking disclosure', () => {
assert.match(markup, /class="[^"]*astryx-chat-reasoning[^"]*"/);
assert.match(markup, /class="maka-assistant-answer-content"/);
assert.match(markup, /role="button"[^>]*aria-expanded="false"/);
// The reasoning body carries the product wrap class so
// `.maka-chat-reasoning-content` in styles.css can restore pre-wrap —
// Astryx's own atoms declare no white-space, so without this seam the
// inherited `normal` collapses every newline in the thinking text.
assert.match(markup, /class="maka-chat-reasoning-content [^"]*"/);
assert.match(markup, /private reasoning/);
assert.doesNotMatch(markup, /data-slot="reasoning-trigger"/);
assert.doesNotMatch(markup, /复制思考过程/);
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/astryx-chat-reasoning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,13 @@ export function ChatReasoning(props: ChatReasoningProps) {
</div>
<div className={isExpanded ? 'xrvj5dj xb0j27v x1tu4anv' : 'xrvj5dj xihq33y xb0j27v'}>
<div className="xb3r6kr x2lwn1j">
<div className="x1xye8es x1f43n9v x141an7d x1ltkj2j x9ynric xv1l7n4">{children}</div>
{/* Product class on the reasoning body: the official component's
atoms deliberately own no white-space (children are assumed
pre-rendered), so without it the inherited `white-space: normal`
collapses every newline in the thinking text. Maka restores the
pre-wrap reading contract on this class — see
`.maka-chat-reasoning-content` in styles.css. */}
<div className="maka-chat-reasoning-content x1xye8es x1f43n9v x141an7d x1ltkj2j x9ynric xv1l7n4">{children}</div>
</div>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
bubble, so it needs its own line and the gap above it. */
.maka-turn-truncation-badge { display: flex; width: fit-content; margin-top: 6px; cursor: help; }
.maka-deep-thinking { min-width: 0; }
/* The reasoning body inside the Astryx ChatReasoning disclosure. The official
atoms own no white-space (the shell assumes children are pre-rendered), so
the inherited `white-space: normal` collapses every newline in thinking
text. Restore the pre-wrap reading contract the pre-Astryx disclosure had
(dropped in the #1748 migration). `word-break` keeps long tokens from
overflowing the box, as the old body did. */
.maka-chat-reasoning-content { white-space: pre-wrap; word-break: break-word; }

@keyframes maka-spin { to { transform: rotate(360deg); } }
.maka-spin { animation: maka-spin 1s linear infinite; }
Expand Down
Loading