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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } 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@@ -187,7 +187,14 @@ function findRulesWithDeclaration(

function extractStaticUiButtonClassNames(src: string): string[] {
const classes = new Set<string>();
const uiButtonBlocks = src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? [];
// A topbar UiButton appears either as a direct element
// (<UiButton>…</UiButton>) or, after the Tooltip migration, as the
// render target of a TooltipTrigger
// (<TooltipTrigger render={<UiButton …/>} className="…">…</TooltipTrigger>).
const uiButtonBlocks = [
...(src.match(/<UiButton\b[\s\S]*?<\/UiButton>/g) ?? []),
...(src.match(/<TooltipTrigger\b[\s\S]*?render=\{<UiButton\b[\s\S]*?<\/TooltipTrigger>/g) ?? []),
];
for (const block of uiButtonBlocks) {
const match = block.match(/\bclassName="([^"]+)"/);
assert.ok(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,9 +83,9 @@ const BARE_PX_RE = /(?<![\w-])-?\d+(?:\.\d+)?px(?![\w-])/;
* any other selector is still caught — add a new caret here only after
* confirming it is geometry, not a stroke. */
const TRIANGLE_CARET_SELECTORS = new Set([
'.maka-turn-thinking summary::before',
'.maka-turn-thinking [data-slot="collapsible-trigger"]::before',
'.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after',
'.maka-permission-raw > summary::before',
'.maka-permission-raw [data-slot="collapsible-trigger"]::before',
]);

const BORDER_STYLE_KEYWORDS = new Set([
Expand DownExpand Up@@ -211,7 +211,7 @@ describe('border-width whitelist negative cases', () => {

it('findCssOffenders allows triangle caret geometry on allowlisted selectors, flags multi-value and single bare-px elsewhere', () => {
// Known caret selectors: multi-value geometry is allowed.
assert.deepEqual(findCssOffenders('.maka-turn-thinking summary::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-turn-thinking [data-slot="collapsible-trigger"]::before {\n border-width: 4px 0 4px 5px;\n}', 't'), [], 'allowlisted caret: 4px 0 4px 5px must pass');
assert.deepEqual(findCssOffenders('.maka-bubble-assistant li.task-list-item > input[type="checkbox"]:checked::after {\n border-width: 0 2px 2px 0;\n}', 't'), [], 'allowlisted caret: 0 2px 2px 0 must pass');
// Non-allowlisted selector: a multi-value bare px is a stroke drift, not
// triangle geometry — a heuristic that spared ALL multi-value would miss it.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ describe('chat Marker shell migration contract (#332 PR2)', () => {
// `.maka-turn-thinking` is explicitly deferred (pseudo-element chevron +
// @starting-style fade don't reduce to leaf utilities); it stays authored.
'.maka-turn-thinking',
'.maka-turn-thinking summary',
'.maka-turn-thinking [data-slot="collapsible-trigger"]',
]) {
assert.ok(css.includes(selector), `out-of-scope turn rule "${selector}" must be preserved`);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,8 +22,10 @@ import { REPO_ROOT, TOKENS_FILE, readAllRendererCss, stripCssComments } from './
* 2. the running status dot's `@keyframes maka-tool-pulse` ring frames — an
* animation can't be a leaf-literal and `getComputedStyle` reads a phase-
* dependent value, so the breath is pinned here + by the `chat.tsx` literal;
* 3. native `<summary>` marker reset — re-keyed off the retired `.maka-tool`
* class onto the governed `[data-slot="tool"]` hook.
* 3. the `[data-slot="tool"]` base residue (opacity/transform/border-color
* transition) — the native `<summary>` marker reset that used to live here is
* gone after the disclosure → Collapsible migration (the trigger is a button
* with no native marker).
*/
describe('chat tool-card migration contract (#332 PR3b)', () => {
it('retires the bespoke tool-card shell selectors (without touching error/preview/maka-code)', async () => {
Expand All@@ -43,7 +45,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'.maka-tool-body',
'.maka-tool-intent',
'.maka-tool-count',
// the `<details>` card base + its status / open / summary selectors
// the retired native-disclosure card base + status/open selectors
'.maka-tool {',
'.maka-tool >',
'.maka-tool[open]',
Expand DownExpand Up@@ -90,7 +92,7 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
}
});

it('keeps only the marker reset residue, re-keyed onto [data-slot="tool"]', async () => {
it('keeps the tool-card base residue on [data-slot="tool"]', async () => {
const tokens = stripCssComments(await readFile(TOKENS_FILE, 'utf8'));
assert.ok(
!tokens.includes('.maka-tool {') && !tokens.includes('@starting-style {\n .maka-tool'),
Expand All@@ -101,8 +103,6 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
'[data-slot="tool"] {',
'transform: translateY(0)',
'transition: border-color var(--duration-base) var(--ease-out-strong);',
'[data-slot="tool"] > summary::-webkit-details-marker { display: none; }',
"[data-slot=\"tool\"] > summary::marker { content: ''; }",
]) {
assert.ok(
tokens.includes(residue),
Expand DownExpand Up@@ -176,12 +176,35 @@ describe('chat tool-card migration contract (#332 PR3b)', () => {
);
}
// The open/collapsed divider — the one card surface that differs by state
// (the collapsed default has no bottom border). The computed-diff proves both
// states, but that harness is manual (no CI), so pin the `[open]>summary`
// literal here as the automated guard.
// (the collapsed default has no bottom border). Base UI puts
// `[data-panel-open]` directly on the Collapsible Trigger, so keep the border
// on the styled trigger/header part without adding a root group or crossing
// elements to read root state.
assert.ok(
block.includes('[&[open]>summary]:[border-bottom:1px_solid_var(--border)]'),
'item must keep the `[open]>summary` divider literal (collapsed default has none)',
!rawSrc.includes('[open]>summary'),
'tool card source must not keep the old native details `[open]>summary` selector, even in comments',
);
assert.ok(
!rawSrc.includes('group-data-[open]/tool'),
'tool card source must not use a root group to read open state when Base UI Trigger exposes [data-panel-open]',
);
const itemStart = block.indexOf('item:');
const headerStart = block.indexOf('header:', itemStart);
const dotStart = block.indexOf('dot:', headerStart);
assert.ok(itemStart !== -1 && headerStart !== -1 && dotStart !== -1, 'toolVariants item/header/dot parts must stay parseable');
const itemBlock = block.slice(itemStart, headerStart);
const headerBlock = block.slice(headerStart, dotStart);
assert.ok(
!itemBlock.includes('group/tool'),
'tool card root must not add a named group for open state when Base UI Trigger exposes [data-panel-open]',
);
assert.ok(
!itemBlock.includes('border-bottom'),
'tool card root must not own the open-state divider; put the border on the trigger/header part',
);
assert.ok(
headerBlock.includes('data-[panel-open]:[border-bottom:1px_solid_var(--border)]'),
'tool card header must add the divider from Base UI Trigger [data-panel-open]',
);
// Anti-drift: pin the distinctive literals and ban the semantic-scale
// forms they would be swapped for. Radius uses the `--radius-surface`
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
/**
* PR-DISCLOSURE-COLLAPSIBLE-0 (issue #520 PR5 item 17, 2026-07-05):
* the four disclosure sites (turn-thinking, reasoning-panel, permission-raw,
* tool-activity) migrate off native `<details>`/`<summary>` onto Base UI
* Collapsible. The code comments at the sites already said "future Base UI
* Accordion path"; all four are independent single sections (not grouped),
* so Collapsible (not Accordion) is the right primitive.
*
* Why migrate: native `<details>` gives free keyboard a11y but no CSS hook for
* the open/closed animation state, no controlled-open API for the reasoning
* panel's "default open, first click sticks" behavior (which today reads
* `e.currentTarget.open` from the toggle event), and no `data-slot` for the
* style-hook convention. Base UI Collapsible gives `data-[open]` state, a
* controlled `open` prop, and the `data-slot` hook.
*
* This contract locks the migration: the three files that held the four
* `<details>` sites must not carry `<details>`/`<summary>` (not even in
* comments — a stale "wrapped in a <details>" comment is a regression
* signal), and must import Collapsible. The Collapsible primitive itself
* must wrap Base UI Collapsible with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'packages/ui/src/chat-view.tsx',
'packages/ui/src/permission-dialog.tsx',
'packages/ui/src/tool-activity.tsx',
];

const COLLAPSIBLE_PRIMITIVE = 'packages/ui/src/primitives/collapsible.tsx';

/** A Collapsible import: from the @maka/ui barrel, the primitives path, or
* @base-ui/react/collapsible directly. */
const COLLAPSIBLE_IMPORT_RE = /import\s+\{[^}]*\bCollapsible\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/collapsible|@base-ui\/react\/collapsible)[^'"]*['"]/;

describe('PR-DISCLOSURE-COLLAPSIBLE-0 contract', () => {
it('the disclosure sites use Base UI Collapsible (no native <details>/<summary>)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!/<details\b/.test(src), `${rel}: must not use native <details> (migrate to Base UI Collapsible; also drop stale <details> mentions in comments)`);
assert.ok(!/<summary\b/.test(src), `${rel}: must not use native <summary> (use Collapsible.Trigger)`);
assert.match(src, COLLAPSIBLE_IMPORT_RE, `${rel}: must import Collapsible from @maka/ui / primitives/collapsible / @base-ui/react/collapsible`);
}
});

it('primitives/collapsible.tsx wraps Base UI Collapsible with data-slot on Root / Trigger / Panel', async () => {
const src = await readFile(resolve(REPO_ROOT, COLLAPSIBLE_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/collapsible/, 'must import from @base-ui/react/collapsible');
for (const slot of ['collapsible', 'collapsible-trigger', 'collapsible-panel']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});

it('tool-activity Collapsible is controlled (open follows item.status), not defaultOpen', async () => {
// A `defaultOpen` card decides open only on first render, so a card that
// defaults open while pending/running would NOT auto-collapse when it
// settles to completed/interrupted — the pre-Collapsible `<details
// open={isOpenByDefault(status)}>` re-evaluated open every render. The
// controlled form (open + onOpenChange, re-synced via useEffect on
// [item.status]) restores that: status change collapses/expands the card,
// the user can still toggle in between.
const src = await readFile(resolve(REPO_ROOT, 'packages/ui/src/tool-activity.tsx'), 'utf8');
assert.ok(!/defaultOpen=/.test(src), 'tool-activity must not use defaultOpen (a running card that defaults open would not auto-collapse when it settles); use controlled open that follows item.status');
assert.match(src, /\bonOpenChange\b/, 'tool-activity Collapsible must be controlled via onOpenChange');
assert.match(src, /useEffect\([^]*\[item\.status\]/, 'tool-activity must re-sync open when item.status changes (useEffect on [item.status])');
});
});

describe('disclosure-collapsible negative cases', () => {
it('flags a native <details> and a missing Collapsible import', () => {
const withDetails = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <details><summary>h</summary>b</details>; }';
assert.ok(/<details\b/.test(withDetails), '<details> must be detected');
const noImport = 'export function X() { return null; }';
assert.ok(!COLLAPSIBLE_IMPORT_RE.test(noImport), 'no Collapsible import must not match');
const withImport = 'import { Collapsible } from "@maka/ui";\nexport function X() { return <Collapsible.Root />; }';
assert.ok(COLLAPSIBLE_IMPORT_RE.test(withImport), 'a Collapsible import must match');
});
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
/**
* PR-NUMBER-FIELD-CONVERGE-0 (issue #520 PR5 item 21, 2026-07-05):
* the two gateway/proxy port inputs migrate off the native `Input` +
* `Number(event.currentTarget.value)` hand-conversion onto Base UI
* NumberField, which binds `value: number | null` directly and parses
* numeric input itself (no manual string→number, no `|| default` fallback
* gymnastics).
*
* Sites:
* - general-settings-page proxy port (default 0 on empty).
* - open-gateway-settings-page gateway port (default 3939 on empty).
*
* The contract: the two files must not carry the `Number(event.currentTarget
* .value)` hand-conversion and must import NumberField; the primitive must
* wrap Base UI NumberField with the data-slot convention (item 23).
*/

import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import { REPO_ROOT } from './css-test-helpers.js';

const MIGRATED_FILES = [
'apps/desktop/src/renderer/settings/general-settings-page.tsx',
'apps/desktop/src/renderer/settings/open-gateway-settings-page.tsx',
];

const NUMBER_FIELD_PRIMITIVE = 'packages/ui/src/primitives/number-field.tsx';

/** The hand-conversion the migration removes. */
const HAND_CONVERT_RE = /Number\(event\.currentTarget\.value\)/;

/** A NumberField import from the barrel / primitives / @base-ui. */
const NUMBER_FIELD_IMPORT_RE = /import\s+\{[^}]*\bNumberField\b[^}]*\}\s+from\s+['"][^'"]*(?:@maka\/ui|primitives\/number-field|@base-ui\/react\/number-field)[^'"]*['"]/;

describe('PR-NUMBER-FIELD-CONVERGE-0 contract', () => {
it('the port-input files use Base UI NumberField (no Number(event.currentTarget.value) hand-conversion)', async () => {
for (const rel of MIGRATED_FILES) {
const src = await readFile(resolve(REPO_ROOT, rel), 'utf8');
assert.ok(!HAND_CONVERT_RE.test(src), `${rel}: must not hand-convert Number(event.currentTarget.value) — use Base UI NumberField (value: number | null, onValueChange)`);
assert.match(src, NUMBER_FIELD_IMPORT_RE, `${rel}: must import NumberField from @maka/ui / primitives/number-field / @base-ui/react/number-field`);
}
});

it('primitives/number-field.tsx wraps Base UI NumberField with data-slot on Root / Input', async () => {
const src = await readFile(resolve(REPO_ROOT, NUMBER_FIELD_PRIMITIVE), 'utf8');
assert.match(src, /@base-ui\/react\/number-field/, 'must import from @base-ui/react/number-field');
for (const slot of ['number-field', 'number-field-input']) {
assert.match(src, new RegExp(`data-slot="${slot}"`), `must expose data-slot="${slot}" (style-hook convention, item 23)`);
}
});
});

describe('number-field negative cases', () => {
it('HAND_CONVERT_RE matches the hand-conversion, not other Number() uses', () => {
assert.ok(HAND_CONVERT_RE.test('Number(event.currentTarget.value)'), 'the hand-conversion must match');
assert.ok(!HAND_CONVERT_RE.test('Number(123)'), 'a plain Number(123) must not match');
assert.ok(!HAND_CONVERT_RE.test('const n = Number("x")'), 'a different Number() call must not match');
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ describe('SearchModal lifecycle contract (PR-SIDEBAR-IA-0 Phase 3 P0 fixup)', ()

it('returns focus to the sidebar Search trigger when the modal closes', async () => {
const main = await readRendererShellCombinedSource();
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/UiButton>/)?.[0] ?? '';
const shellSearchButton = main.match(/className="maka-shell-topbar-button"[\s\S]*?data-maka-search-trigger="true"[\s\S]*?<\/TooltipTrigger>/)?.[0] ?? '';
const closeSearchModal = main.match(/function closeSearchModal\(options\?: \{ restoreFocus\?: boolean \}\) \{[\s\S]*?\n \}/)?.[0] ?? '';

assert.match(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
networkBlock,
/value=\{String\(proxyDraft\.port \|\| ''\)\}[\s\S]*onChange=\{\(event\) => void updateProxy\(\{ port: Number\(event\.currentTarget\.value\) \|\| 0 \}\)\}/,
/value=\{proxyDraft\.port \|\| null\}[\s\S]*onValueChange=\{\(v\) => void updateProxy\(\{ port: v \?\? 0 \}\)\}/,
'Network proxy port input must render from the local draft while persisting in the background',
);
assert.match(
Expand DownExpand Up@@ -231,7 +231,7 @@ describe('Settings network and gateway persistence contract', () => {
);
assert.match(
gatewayBlock,
/value=\{String\(gatewayDraft\.port\)\}[\s\S]*onChange=\{\(event\) => void updateGateway\(\{ port: Number\(event\.currentTarget\.value\) \|\| 3939 \}\)\}/,
/value=\{gatewayDraft\.port\}[\s\S]*onValueChange=\{\(v\) => void updateGateway\(\{ port: v \?\? 3939 \}\)\}/,
'Open Gateway port input must render from the local draft while persisting in the background',
);
assert.doesNotMatch(
Expand Down
Loading