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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
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
Expand Up@@ -104,8 +104,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(src, /aria-busy=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /data-pending=\{pendingArtifactListRetry \? 'true' : undefined\}/);
assert.match(src, /pendingArtifactListRetry \? '重试中…' : '重试'/);
assert.match(css, /\.maka-artifact-error-retry:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-error-retry\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"/);
assert.doesNotMatch(css, /\.maka-artifact-error-retry/);
assert.doesNotMatch(src, /className="maka-artifact-error-retry"[\s\S]*onClick=\{\(\) => void refresh\(\)\}/);
assert.match(
subscriptionEffect,
Expand DownExpand Up@@ -211,7 +211,8 @@ describe('ArtifactPane async lifecycle contract', () => {
assert.match(toolbarBlock, /另存中…/);
assert.match(toolbarBlock, /复制中…/);
assert.match(toolbarBlock, /删除中…/);
assert.match(css, /\.maka-artifact-toolbar-button:disabled \{[\s\S]*cursor: default;[\s\S]*opacity: var\(--opacity-disabled\);[\s\S]*\}/);
assert.match(css, /\.maka-artifact-toolbar-button\[data-pending="true"\] \{[\s\S]*opacity: var\(--opacity-pending\);[\s\S]*\}/);
assert.doesNotMatch(css, /\.maka-artifact-toolbar-button\b/, 'artifact actions must not restore consumer-owned Button states');
assert.match(toolbarBlock, /variant="secondary"\s+size="sm"/);
assert.match(toolbarBlock, /variant="destructive" size="icon-sm"/);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,8 +307,8 @@ describe('Bot settings UI contract', () => {
assert.match(settings, /window\.setInterval\(\(\) => \{[\s\S]*reloadQrCode\(\)/, 'WeChat bridge QR polling must not bypass the reload guard');
assert.match(settings, /setResult\(\{[\s\S]*ok: false,[\s\S]*error: settingsActionErrorMessage\(error\),[\s\S]*hint: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。'/, 'WeChat bridge QR thrown failures must use the Settings scrubber before rendering');
assert.doesNotMatch(settings, /error: error instanceof Error \? error\.message : String\(error\)/, 'WeChat bridge QR modal must not render raw thrown Error.message');
assert.match(settings, /className="settingsWechatQrSecondary" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must disable while a QR reload is in flight');
assert.match(styles, /\.settingsWechatQrSecondary:disabled\s*\{[\s\S]*cursor:\s*progress/, 'WeChat bridge QR reload buttons must have a visible pending state');
assert.match(settings, /variant="secondary" size="sm" disabled=\{loading\} onClick=\{reloadQrCode\}/, 'WeChat bridge QR refresh buttons must use the governed compact tier and disable while a QR reload is in flight');
assert.doesNotMatch(styles, /\.settingsWechatQrSecondary\b/, 'WeChat QR actions must not restore consumer-owned Button states');
assert.match(settings, /window\.maka\.settings\.bots\.wechatQrCode\(\)/, 'QR modal must call the bridge QR IPC');
assert.match(settings, /<img src=\{qrDataUrl\} alt="微信扫码登录二维码"/, 'QR modal must render a visible QR image');
assert.match(settings, /setWechatQrOpen\(true\)/, 'Scan-login button must open the QR modal');
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ async function workspaceTopActionButtonCount(): Promise<number> {
const start = source.indexOf('export function AppShellWorkspaceTopActions');
assert.notEqual(start, -1, 'AppShellWorkspaceTopActions should exist');
const block = source.slice(start);
return [...block.matchAll(/className="maka-workspace-icon-action"/g)].length;
return [...block.matchAll(/render=\{<UiButton variant="quiet" size="icon-sm" \/>\}/g)].length;
}

describe('chat header actions inset contract', () => {
Expand DownExpand Up@@ -98,20 +98,16 @@ describe('chat header actions inset contract', () => {
const css = await readRendererContractCss();
const buttonCount = await workspaceTopActionButtonCount();
const toolbarBody = ruleBody(css, '.maka-workspace-top-actions');
const iconBody = ruleBody(css, '.maka-workspace-icon-action');

const buttonWidth = pxDeclaration(iconBody, 'width');
const buttonHeight = pxDeclaration(iconBody, 'height');
const buttonSize = 28;
const gap = pxDeclaration(toolbarBody, 'gap');
const insetAddend = workspaceTopActionsInsetAddend(css);

assert.equal(buttonCount, 4, 'current top-actions toolbar renders four icon buttons');
assert.equal(buttonWidth, 24, 'top-actions icon buttons are 24px wide');
assert.equal(buttonHeight, buttonWidth, 'top-actions icon buttons should stay square');
assert.equal(buttonSize, 28, 'top-actions use the governed compact Button tier');
assert.equal(gap, 6, 'top-actions icon buttons use a 6px gap');
assert.equal(
insetAddend,
(buttonCount * buttonWidth) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
(buttonCount * buttonSize) + ((buttonCount - 1) * gap) + CHAT_HEADER_TOOLBAR_CLEARANCE_PX,
'the chat-header inset addend must match the rendered toolbar footprint plus 12px clearance',
);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
}
});

it('pins the Marker variants to the retired turn-marker pixels/tokens', async () => {
it('pins Marker semantics while shared Button owns action geometry and states', async () => {
const rawSrc = await readFile(
resolve(REPO_ROOT, 'packages', 'ui', 'src', 'primitives', 'chat.tsx'),
'utf8',
Expand DownExpand Up@@ -91,18 +91,6 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// default and revealed on hover / focus-within of the answer block
// (`group/answer`), replacing the retired quiet-0.72 + settle fade-in.
'opacity-0 [transition:opacity_var(--duration-quick)_var(--ease-out-strong)] group-hover/answer:opacity-100 focus-within:opacity-100',
'min-h-[28px]',
// `h-8` (→30px) is folded into the footer-action / lineage-badge shells
// now that the call sites use `UiButton size="nav"` (bare); it used to
// come implicitly from `size="sm"`.
'h-8',
'[&:hover:not([aria-disabled=true])]:bg-[oklch(from_var(--foreground)_l_c_h_/_0.05)]',
// focus-visible is a non-leaf conflict (the footer action's outline vs
// UiButton's box-shadow ring), so the rendered-style script can't force
// it reliably; this exact literalization of the retired
// `outline: 2px solid var(--focus-ring)` pins it here instead.
'focus-visible:[outline:2px_solid_var(--focus-ring)]',
'focus-visible:[outline-offset:2px]',
'data-[pending=true]:opacity-[0.78]',
// the combined aria-disabled+pending guard: a copy button can be both
// `aria-disabled` and `data-pending` (transient copy click), and the
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,11 +138,12 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.doesNotMatch(textareaLine!, /min-h-[a-z0-9]+/i, '.maka-composer-textarea className must not carry a Tailwind min-h-* utility (CSS min-height: var(--h-composer-min) is the single source)');
});

it('stop button uses an h-8 size (icon-sm or sm, 32px) — streaming toolbar height matches send (icon-sm/32px, locked by control-height-converge-contract), no 4px chat-boundary jump', async () => {
it('stop and send use the governed 32px tier so streaming does not change toolbar height', async () => {
const source = await readFile(COMPOSER_TSX, 'utf8');
const stopBlock = source.match(/props\.streaming\s*\?\s*\(\s*<UiButton[\s\S]*?<\/UiButton>/);
assert.ok(stopBlock, 'stop button block (streaming branch) not found');
assert.match(stopBlock[0], /size="(?:icon-sm|sm)"/, 'stop button must use size="icon-sm" or size="sm" (h-8/32px), NOT default md (h-9/36px) which jumps the chat boundary 4px when streaming swaps send→stop; send is locked to 32px by control-height-converge-contract (.maka-composer-send-button height = --h-control-lg = 32px)');
assert.match(stopBlock[0], /size="md"/);
assert.match(source, /variant="default"\s+size="icon"[\s\S]*aria-label=\{buttonCopy\.sendLabel\}/);
});

it('negative cases: same-block duplicate, selector-list companion, compound .maka-composer.composer padding return, .maka-composer padding return, textarea min-h-* return, stop md return', () => {
Expand DownExpand Up@@ -173,4 +174,4 @@ describe('PR-COMPOSER-CONSTANT-FOOTPRINT-0 contract (issue #740)', () => {
assert.ok(stopMdBlock, 'stop block extraction must work on the fixture');
assert.throws(() => assert.match(stopMdBlock[0]!, /size="(?:icon-sm|sm)"/), 'a stop button defaulting to md (no size) must be caught end-to-end (h-9/36px ≠ send h-8/32px)');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,10 +89,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// sidebar / 会话 rows
{ selector: '.maka-list-row', props: ['min-height'], token: '--h-control-lg' },
{ selector: '.maka-list-row-menu-trigger', props: ['width', 'height'], token: '--h-control-lg' },
// .maka-search-modal-close retired: the close button is the shared
// DialogHeader's quiet icon-sm Button, sized by buttonVariants, not a
// search-modal-specific class.
{ selector: '.maka-search-modal-clear', props: ['width', 'height'], token: '--h-control-sm' },
// Search close and clear actions are shared quiet icon-sm Buttons, sized by
// buttonVariants rather than search-modal-specific CSS.
// 设置 nav / triggers
{ selector: '.settingsBackButton', props: ['height', 'min-height'], token: '--h-control-xl' },
{ selector: '.settingsNavItem', props: ['height', 'min-height'], token: '--h-control-xl' },
Expand All@@ -102,13 +100,8 @@ const CONTROL_HEIGHT: ControlHeightCheck[] = [
// chat-header / palette controls
{ selector: '.maka-chat-jump-bottom', props: ['width', 'height'], token: '--h-control-md' },
{ selector: '.maka-palette-input-wrap', props: ['min-height'], token: '--h-control-lg' },
// first-run checklist rows
{ selector: '.maka-first-run-checklist-error-action', props: ['min-height'], token: '--h-control-sm' },
// first-run checklist composite rows
{ selector: '.maka-first-run-checklist-row > button', props: ['min-height'], token: '--h-control-xl' },
// composer controls
{ selector: '.maka-composer-send-button', props: ['width', 'height'], token: '--h-control-lg' },
{ selector: '.maka-composer-workspace-picker', props: ['min-height'], token: '--h-control-sm' },
{ selector: '.maka-composer-branch-picker', props: ['min-height'], token: '--h-control-sm' },
];

/** Values that are always allowed (not a control-height beat). `100%`
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__tests__/explore-agent-tool.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js';

const repoRoot = join(process.cwd(), '..', '..');

describe('ExploreAgent read-only worker', () => {
it('exposes a permission-gated subagent tool', () => {
const tool = buildExploreAgentTool();
Expand DownExpand Up@@ -554,6 +556,9 @@ describe('ExploreAgent read-only worker', () => {
'ExploreAgent copy actions should keep the governed previewVariants agent-copy part on shared UiButton controls',
);
assert.doesNotMatch(previewBlock, /\bmaka-explore-agent-copy\b/);
const chatPrimitive = await readFile(join(repoRoot, 'packages/ui/src/primitives/chat.tsx'), 'utf8');
const agentCopyVariant = chatPrimitive.match(/"agent-copy":\s*[\s\S]*?data-\[copy-error=true\][^,]+/)?.[0] ?? '';
assert.doesNotMatch(agentCopyVariant, /\b(?:gap-|min-h-|px-|py-|text-xs)\b/);
assert.doesNotMatch(previewBlock, /data-size="sm"/);
assert.match(previewBlock, /复制中…/);
assert.match(previewBlock, /复制失败/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,8 +206,9 @@ describe('FIRST_RUN_TASK_SUGGESTIONS', () => {
assert.match(source, /部分状态暂时没刷新成功,已避免把未知状态计成未完成/);
assert.match(source, /role="alert"/);
assert.match(styles, /\.maka-first-run-checklist-error\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action\s*\{/);
assert.match(styles, /\.maka-first-run-checklist-error-action:disabled\s*\{/);
assert.match(source, /variant="secondary"\s+size="sm"[\s\S]*refreshChecklistStatus/);
assert.doesNotMatch(source, /className="maka-first-run-checklist-error-action"/);
assert.doesNotMatch(styles, /\.maka-first-run-checklist-error-action/);
});

it('starts the shipped plan reminder form from the first-run checklist', async () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ describe('Plan Reminder panel async action contract', () => {
);
assert.match(panelBlock, /const submitDisabled = !canCreate \|\| submitPending;/);
assert.match(panelBlock, /<form className="maka-plan-form" onSubmit=\{submit\} aria-busy=\{submitPending \? 'true' : undefined\}>/);
assert.match(panelBlock, /<UiButton className="maka-button maka-plan-submit" type="submit" disabled=\{submitDisabled\}>/);
assert.match(panelBlock, /<UiButton type="submit" disabled=\{submitDisabled\}>/);

assert.match(
refreshBlock,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -370,7 +370,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.maka-code': '--radius-surface',
'.maka-skeleton-card': '--radius-surface',
'.composer .maka-composer-inner': '--radius-modal',
'.settingsPermissionRefresh': '--radius-control',
'.settingsCapabilityGuidanceActions code': '--radius-surface',
'.settingsModal': '--radius-modal',
'.maka-palette-modal': '--radius-modal',
Expand All@@ -380,7 +379,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.settingsOsPermissionList': '--radius-surface',
'.settingsHealthIntro': '--radius-surface',
'.settingsHealthError': '--radius-surface',
'.settingsHealthRefresh': '--radius-control',
'.settingsBotRuntime': '--radius-surface',
'.settingsNotice': '--radius-surface',
'.settingsAboutLogo': '--radius-surface',
Expand All@@ -390,7 +388,6 @@ describe('radius token governance (#406 gap 4)', () => {
'.providerUnavailableNotice': '--radius-surface',
'.enabledEmptyChip': '--radius-control',
'.maka-firstrun-list': '--radius-surface',
'.maka-onboarding-quickchat-submit': '--radius-control',
'.maka-first-run-checklist': '--radius-surface',
'.providerLogo': '--radius-surface',
'.maka-browser-address': '--radius-control',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ test('renderer error boundary exposes a redacted copyable diagnostic report', as
// lock for 复制中…/已复制 feedback). Match the class as a whole word in the
// class list instead of an exact className="…", same form as the negative
// maka-button check below.
assert.match(source, /variant="outline"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /variant="secondary"[\s\S]*className="[^"]*\bmaka-error-copy-action\b[^"]*"/);
assert.match(source, /<UiButton type="button" variant="secondary" onClick=\{this\.handleReset\}>/);
assert.match(source, /<UiButton[\s\S]*variant="default"[\s\S]*onClick=\{this\.handleReload\}/);
assert.doesNotMatch(source, /className="maka-button/);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,12 +55,8 @@ describe('renderer style layer cascade contract', () => {
/**
* Regression guard for #257 / #253 Round A.
*
* The sidebar nav rows render as `<UiButton size="nav" className="maka-nav-row">`
* (packages/ui/src/components.tsx). The cva button base always carries the
* Tailwind utilities `inline-flex items-center justify-center`, and the
* `nav` size variant deliberately contributes NO layout utilities so that
* `.maka-nav-row` (display: grid + grid-template-columns + text-align: left)
* is the layout source of truth.
* Sidebar rows are semantic Base UI navigation controls whose grid layout
* lives in `.maka-nav-row`.
*
* That only holds while `.maka-nav-row` outranks the utilities. #257 wrapped
* styles.css into `@layer base`/`@layer components`; because Tailwind v4
Expand All@@ -77,30 +73,45 @@ describe('renderer style layer cascade contract', () => {
layers,
0,
`.maka-nav-row is nested in ${layers} @layer block(s); it must stay unlayered to ` +
'override the cva button base utilities (inline-flex/justify-center). See #257 regression.',
'remain the authoritative semantic navigation-row layout. See #257 regression.',
);
});

it('keeps .settingsHealthRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps the composite session target on the same control radius as its row action', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsHealthRefresh {');
assert.notEqual(layers, -1, '.settingsHealthRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsHealthRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
assert.match(
styles,
/\.maka-list-row-main\s*\{[^}]*border-radius:\s*var\(--radius-control\);[^}]*\}/,
'the semantic session target must own the same governed radius as .maka-list-row-menu-trigger',
);
});

it('keeps .settingsPermissionRefresh out of any @layer so it can override secondary Button utilities', async () => {
it('keeps composite session-list controls on one complete interaction contract', async () => {
const styles = await readAllRendererCss();
const layers = enclosingLayerCount(styles, '.settingsPermissionRefresh {');
assert.notEqual(layers, -1, '.settingsPermissionRefresh { rule not found in renderer CSS');
assert.equal(
layers,
0,
'.settingsPermissionRefresh must stay unlayered because it overrides the shared secondary Button utility stack (background/border/padding/color).',
);
const controls = ':is\\(\\.maka-list-group-toggle, \\.maka-list-project-heading, \\.maka-list-project-more\\)';

assert.match(styles, new RegExp(`${controls}:hover\\s*\\{[^}]*background:\\s*var\\(--state-hover-bg\\);`));
assert.match(styles, new RegExp(`${controls}:active\\s*\\{[^}]*background:\\s*var\\(--state-selected-bg\\);`));
assert.match(styles, new RegExp(`${controls}:focus-visible\\s*\\{[^}]*outline:\\s*var\\(--focus-ring-width\\) solid var\\(--focus-ring\\);`));
assert.match(styles, new RegExp(`${controls}:disabled\\s*\\{[^}]*opacity:\\s*var\\(--opacity-disabled\\);`));
});

it('keeps settings utility actions on governed Button variants instead of unlayered reskins', async () => {
const [styles, permission, health, password] = await Promise.all([
readAllRendererCss(),
readFile('src/renderer/settings/permission-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/health-center-page.tsx', 'utf8'),
readFile('src/renderer/settings/password-input.tsx', 'utf8'),
]);

assert.match(permission, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*重新检测/);
assert.match(health, /<Button\s+type="button"\s+variant="secondary"\s+size="sm"[\s\S]*?>\s*刷新/);
assert.equal(password.match(/variant="quiet"\s+size="icon-sm"/g)?.length, 2);

for (const legacyClass of ['settingsPermissionRefresh', 'settingsHealthRefresh', 'settingsPasswordToggle']) {
assert.doesNotMatch(`${permission}\n${health}\n${password}`, new RegExp(`className="${legacyClass}"`));
assert.doesNotMatch(styles, new RegExp(`\\.${legacyClass}(?:\\s|:|\\{|\\[)`));
}
});

/**
Expand Down
Loading
Loading