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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT, readAllRendererCss, stripCssComments } from './css-test-helpers.js';

/**
* Zero-visual governance contract for issue #332 PR1 — the chat
* conversation-flow row/bubble *shell* moved onto the `@maka/ui` `Message` /
* `Bubble` primitives. These assertions lock the two halves of "zero visual
* change": the bespoke shell CSS is retired, while the Markdown prose and the
* still-hand-written turn machinery (PR2) keep their exact layout.
*/
describe('chat primitive shell migration contract (#332 PR1)', () => {
it('retires the bespoke bubble/row shell selectors', async () => {
const css = stripCssComments(await readAllRendererCss());
for (const selector of [
'.maka-bubble-user',
'.maka-bubble-truncated',
'.maka-bubble-assistant-stack',
'.message.user',
'.message.assistant',
'.message.system',
'.message >',
'.message pre',
]) {
assert.ok(
!css.includes(selector),
`retired shell selector "${selector}" still present in renderer CSS`,
);
}
});

it('preserves the assistant Markdown prose (OUT of scope)', async () => {
const css = await readAllRendererCss();
for (const selector of [
'.maka-bubble-assistant {',
'.maka-bubble-assistant p',
'.maka-bubble-assistant pre',
'.maka-bubble-assistant table',
'.maka-bubble-assistant li.task-list-item',
]) {
assert.ok(css.includes(selector), `prose rule "${selector}" must be preserved`);
}
});

it('keeps the row + re-anchors turn layout onto the Message primitive', async () => {
const css = await readAllRendererCss();
// The centered reading column / entrance animation stay authored.
assert.ok(css.includes('.maka-message-row'), '.maka-message-row row base must stay');
// Lineage row + footer (PR2, still hand-written) ride the primitive's
// data hook so their measure column survives until they migrate.
assert.ok(
css.includes('[data-slot="message"][data-role="assistant"] .maka-turn-footer'),
'turn footer layout must be re-anchored to the Message primitive',
);
assert.ok(
css.includes('[data-slot="message"][data-role="system"] pre'),
'system note pre styling must be re-anchored to the Message primitive',
);
});

it('pins the user bubble shell to the retired .maka-bubble-user pixels', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
);
// Strip comments so the assertions reflect real classNames, not prose that
// happens to name the scale utilities it is telling us to avoid.
const chatSrc = rawSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
// The shell values are LITERAL Tailwind arbitrary utilities, so the variant
// class string compiles 1:1 to its declarations on a leaf element with
// nothing to resolve or override — asserting the exact string here is
// equivalent to asserting the computed style, without a browser. Matching
// the WHOLE string (not just "contains each literal") also pins the set
// closed: a stray extra `rounded-[12px]` / `px-4` / second `max-w-*` that
// would silently override the shell makes this fail. Values mirror the
// retired `.maka-bubble-user` exactly (border-radius:10px; padding:10px
// 14px; line-height:1.6; max-width:min(100%,640px); --chat-user-bg) and
// never the semantic scale (`rounded-lg`, `px-3.5`) or primary/accent.
const bubbleBlock = chatSrc.slice(chatSrc.indexOf('bubbleVariants'));
const userClass = bubbleBlock.match(/user:\s*"([^"]*)"/)?.[1];
assert.equal(
userClass,
'max-w-[min(100%,640px)] whitespace-pre-wrap break-words rounded-[10px] bg-[var(--chat-user-bg)] px-[14px] py-[10px] leading-[1.6] text-[color:var(--chat-user-foreground,var(--foreground))]',
'user bubble variant must match the retired .maka-bubble-user pixels exactly',
);
});
});
36 changes: 4 additions & 32 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1132,12 +1132,6 @@
transform: rotate(45deg);
}

.maka-bubble-assistant-stack {
display: flex;
flex-direction: column;
gap: 0;
}

/* The streaming bubble (out-of-band, before the in-progress turn is
* fully persisted) sits at the bottom of the chat surface; give it the
* same horizontal frame as a real turn message. */
Expand All@@ -1147,25 +1141,10 @@
previous turn while it lands. */
box-sizing: border-box;
}
/* User message: a tinted, width-capped block anchored to the right.
PR-CHAT-CHROME-FOLLOWUP-0: the previous treatment was transparent
right-aligned text — for a long message it filled most of the
column, so the right-anchor was imperceptible. Re-tinting with the
existing `--chat-user-bg` token + a width cap makes "what the human
said" read clearly on the right at any length. The block shrink-
wraps to its content because `.message.user` aligns items to
flex-end (so short messages stay compact). Radius 10px matches the
code-block family and stays under the sharp-identity ceiling. */
.maka-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: 10px;
padding: 10px 14px;
line-height: 1.6;
max-width: min(100%, 78%);
white-space: pre-wrap;
word-wrap: break-word;
}
/* User message: a tinted, width-capped block anchored to the right — now
the `Bubble variant="user"` chat primitive (issue #332 PR1). It keeps the
neutral `--chat-user-bg` token path; the shell utilities live in
packages/ui/src/primitives/chat.tsx. */
/* Assistant: no bubble — just text on background, like an editor.
The bubble itself flows naturally; child markdown elements (h*, p, ul,
code blocks, tables, blockquotes) all need their own spacing/typography
Expand DownExpand Up@@ -1443,13 +1422,6 @@
border-top: 1px solid var(--border);
}

/* User content stays verbatim text, but shares the same no-bubble body
language as assistant messages. */
.maka-bubble-user {
white-space: pre-wrap;
max-width: min(100%, 640px);
}

/* Hover-revealed copy button on assistant messages. The wrapper is
position: relative so the button can anchor to the top-right corner.
The hover/focus-within guard keeps it out of the way until the user
Expand Down
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/styles/reasoning-panel.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,23 +108,10 @@
cursor: help;
}

/* PR-UI-Cx (@kenji msg cd09bcac) — "已截断" pill on the streaming
assistant bubble. Fires when `applyAssistantDelta` either tail-kept
a single oversize delta or head-capped the per-session total. Same
visual family as the reasoning-panel truncated pill, positioned as
a footer affordance under the bubble's Markdown body so it doesn't
compete with the running text. */
.maka-bubble-truncated {
display: inline-block;
margin-top: 6px;
font-size: 10px;
color: var(--warning-text, var(--info-text));
border: 1px solid oklch(from var(--warning) l c h / 0.24);
background: oklch(from var(--warning) l c h / 0.05);
border-radius: 4px;
padding: 0 5px;
cursor: help;
}
/* The streaming "已截断" pill (PR-UI-Cx, @kenji msg cd09bcac) moved onto the
`Bubble variant="assistant"` chat primitive as inline utilities (issue
#332 PR1); its sibling `.maka-reasoning-panel-truncated` pill below keeps
the same visual family. */

.maka-reasoning-panel-chevron {
font-size: 12px;
Expand Down
47 changes: 10 additions & 37 deletions apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@
animation: none;
}

.message > span {
display: block;
margin-bottom: 6px;
color: var(--foreground-50);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
}

.message.user > span { color: var(--foreground-60); }
.message.assistant > span { color: var(--accent); }
.message.system > span { color: var(--info-text); }

/* PR-CHAT-CHROME-FOLLOWUP-0: the relative time is always visible now
(it was `opacity: 0` until hover, which hid it on touch + from
assistive tech). It stays quiet — 12px at the shared chrome size,
Expand All@@ -49,28 +36,22 @@
}

/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `.message.user` aligns items
to flex-end, so this row shrink-wraps and hugs the right edge). */
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge). */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: 6px;
}

.message.user {
display: flex;
flex-direction: column;
align-items: flex-end;
/* Separate the meta row from the bubble. Kept below the 8px turn
rhythm so the time + copy still read as belonging to the bubble
(proximity) rather than floating between turns. */
gap: 6px;
}

/* Turn summary / lineage / footer share the assistant turn's left-anchored
measure column. The `[data-role="assistant"]` parts ride the chat Message
primitive (issue #332 PR1) — the lineage row + footer are still hand-written
(PR2), so this rule keeps their layout until they migrate. */
.maka-turn > .maka-turn-summary,
.maka-turn > .maka-turn-lineage-row,
.message.assistant .maka-turn-lineage-row,
.message.assistant .maka-turn-footer {
[data-slot="message"][data-role="assistant"] .maka-turn-lineage-row,
[data-slot="message"][data-role="assistant"] .maka-turn-footer {
display: flex;
max-width: var(--maka-chat-measure, 680px);
width: 100%;
Expand All@@ -79,20 +60,12 @@
justify-content: flex-start;
}

.message.assistant,
.message.system {
max-width: var(--maka-chat-measure, 680px);
width: 100%;
margin-left: 0;
margin-right: auto;
}

.message pre {
[data-slot="message"] pre {
margin: 0;
font: inherit;
}

.message.system pre {
[data-slot="message"][data-role="system"] pre {
display: inline-flex;
border-radius: 999px;
background: oklch(from var(--info) l c h / 0.08);
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/__tests__/chat-primitives.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Bubble, Message } from '../primitives/chat.js';

// The re-anchored renderer selectors key off the primitives' own `data-slot` /
// `data-role` / `data-variant`, so a consumer must never be able to clobber
// them. Both primitives are hook-free pure functions, so calling them directly
// and inspecting the returned element's props proves the structural hooks win
// over conflicting props — no DOM, no renderer needed.
test('Message keeps its own data-slot/data-role over conflicting props', () => {
const el = Message({
variant: 'assistant',
'data-slot': 'spoofed',
'data-role': 'user',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'message');
assert.equal(props['data-role'], 'assistant');
});

test('Bubble keeps its own data-slot/data-variant over conflicting props', () => {
const el = Bubble({
variant: 'user',
'data-slot': 'spoofed',
'data-variant': 'assistant',
} as never);
const props = el.props as Record<string, unknown>;
assert.equal(props['data-slot'], 'bubble');
assert.equal(props['data-variant'], 'user');
});
Loading