From 4e0e73579784dd0889e05ada7162487a68696231 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 18 Jul 2026 19:52:32 +0800 Subject: [PATCH 1/4] test(visual-smoke): add module-mcp fixture scenario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeds the 扩展 → MCP module surface (sidebarSection 'mcp') with an mcp.json holding two disabled servers, so the market grid, tab row, hero banner and installed server list all render deterministically for the alignment auditor and CDP capture. enabled:false keeps visual-smoke from attempting real npx / HTTP connections, so rows settle in the neutral 已停用 state. --- .../__tests__/visual-smoke-fixture.test.ts | 35 +++++++++++++++- apps/desktop/src/main/visual-smoke-fixture.ts | 40 +++++++++++++++++++ packages/core/src/visual-smoke.ts | 1 + 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/__tests__/visual-smoke-fixture.test.ts b/apps/desktop/src/main/__tests__/visual-smoke-fixture.test.ts index d212681be07..1b6826b81fb 100644 --- a/apps/desktop/src/main/__tests__/visual-smoke-fixture.test.ts +++ b/apps/desktop/src/main/__tests__/visual-smoke-fixture.test.ts @@ -627,20 +627,53 @@ describe('visual smoke fixture mode', () => { } }); - it('module fixtures open Skills and Daily Review in the app module surface', async () => { + it('module fixtures open Skills, MCP and Daily Review in the app module surface', async () => { const skills = resolveVisualSmokeFixture('module-skills', false); + const mcp = resolveVisualSmokeFixture('module-mcp', false); const dailyReview = resolveVisualSmokeFixture('module-daily-review', false); assert.ok(skills); + assert.ok(mcp); assert.ok(dailyReview); assert.equal(getVisualSmokeState(skills)?.sidebarSection, 'skills'); assert.equal(getVisualSmokeState(skills)?.sidebarCollapsed, false); assert.equal(getVisualSmokeState(skills)?.activeSessionId, 'visual-smoke-turn'); + assert.equal(getVisualSmokeState(mcp)?.sidebarSection, 'mcp'); + assert.equal(getVisualSmokeState(mcp)?.sidebarCollapsed, false); + assert.equal(getVisualSmokeState(mcp)?.activeSessionId, 'visual-smoke-turn'); assert.equal(getVisualSmokeState(dailyReview)?.sidebarSection, 'daily-review'); assert.equal(getVisualSmokeState(dailyReview)?.sidebarCollapsed, false); assert.equal(getVisualSmokeState(dailyReview)?.activeSessionId, 'visual-smoke-turn'); }); + it('module-mcp seeds a couple of installed servers so the 已安装 list renders', async () => { + const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-visual-smoke-mcp-')); + try { + const fixture = resolveVisualSmokeFixture('module-mcp', false); + assert.ok(fixture); + await seedVisualSmokeFixture({ + workspaceRoot, + fixture, + credentialStore: fakeCredentialStore(), + now: 1_700_000_000_000, + }); + const config = JSON.parse(await readFile(join(workspaceRoot, 'mcp.json'), 'utf8')) as { + version: number; + mcpServers: Record; + }; + assert.equal(config.version, 1); + const ids = Object.keys(config.mcpServers); + assert.ok(ids.length >= 2, 'installed list needs >=2 servers to render meaningfully'); + // enabled:false keeps visual-smoke deterministic — no real npx / HTTP + // connection is attempted, so the rows settle in the neutral 已停用 state. + for (const id of ids) { + assert.equal(config.mcpServers[id].enabled, false, `${id} stays disabled in the fixture`); + } + } finally { + await rm(workspaceRoot, { recursive: true, force: true }); + } + }); + it('module-skills seeds a managed-source market catalog (>=6 entries with categories) plus workspace skills', async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-visual-smoke-skills-')); const previousSourcesRoot = process.env.MAKA_SKILL_SOURCES_ROOT; diff --git a/apps/desktop/src/main/visual-smoke-fixture.ts b/apps/desktop/src/main/visual-smoke-fixture.ts index 904ac02a77b..5ce100bd0a3 100644 --- a/apps/desktop/src/main/visual-smoke-fixture.ts +++ b/apps/desktop/src/main/visual-smoke-fixture.ts @@ -63,6 +63,11 @@ const VISUAL_SMOKE_SCENARIOS = new Set([ 'settings-usage', 'settings-health', 'module-skills', + // MCP module page (SVG-governance + polish campaign): opens the 扩展 → MCP + // surface with a seeded mcp.json so the market grid, tab row, hero banner, + // and the installed server list all render for the alignment auditor + CDP + // capture in light / dark. + 'module-mcp', 'module-daily-review', // PR109b: workstation-statuses — seed one session per SessionStatus // (running / waiting_for_user / blocked × 4 reasons / active / review @@ -418,6 +423,10 @@ export function getVisualSmokeState(fixture: VisualSmokeFixture | null): VisualS return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'health' }; case 'module-skills': return { ...state, activeSessionId: TURN_SESSION_ID, sidebarSection: 'skills', sidebarCollapsed: false }; + case 'module-mcp': + // Open the 扩展 → MCP module directly so the CDP baseline captures the + // real market grid, tab row, hero banner, and installed server list. + return { ...state, activeSessionId: TURN_SESSION_ID, sidebarSection: 'mcp', sidebarCollapsed: false }; case 'module-daily-review': return { ...state, activeSessionId: TURN_SESSION_ID, sidebarSection: 'daily-review', sidebarCollapsed: false }; case 'workstation-statuses': @@ -603,6 +612,37 @@ export async function seedVisualSmokeFixture(input: { if (input.fixture.scenario === 'module-skills') { await seedSkillsMarketFixture(input.workspaceRoot); } + if (input.fixture.scenario === 'module-mcp') { + await seedMcpFixture(input.workspaceRoot); + } +} + +/** + * MCP module fixture: seeds an mcp.json with a couple of installed servers so + * the 已安装 tab and the server rows render for the alignment auditor + CDP + * capture. Both are `enabled: false` so no real `npx` / HTTP connection is + * attempted in visual-smoke mode — the rows render deterministically in the + * neutral 已停用 state (exception-only status: no color unless a real failure). + * The 市场 tab is the default surface and is driven by the static MCP_CATALOG, + * so it renders without any on-disk seed. + */ +async function seedMcpFixture(workspaceRoot: string): Promise { + const config = { + version: 1, + mcpServers: { + filesystem: { + enabled: false, + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '/workspace/maka'], + }, + 'linear-remote': { + enabled: false, + url: 'https://mcp.linear.app/sse', + transport: 'sse', + }, + }, + }; + await writeJson(join(workspaceRoot, 'mcp.json'), config); } /** diff --git a/packages/core/src/visual-smoke.ts b/packages/core/src/visual-smoke.ts index daf24349773..3b857047711 100644 --- a/packages/core/src/visual-smoke.ts +++ b/packages/core/src/visual-smoke.ts @@ -50,6 +50,7 @@ export type VisualSmokeScenario = | 'settings-usage' | 'settings-health' | 'module-skills' + | 'module-mcp' | 'module-daily-review' | 'workstation-statuses' | 'plan-reminders' From 221aafd92c695d77febef36d65cef637ae7d34a1 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 18 Jul 2026 19:52:50 +0800 Subject: [PATCH 2/4] feat(mcp): polish MCP page onto the house language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converge the MCP module page onto the same bar as the 技能 page and the 每日回顾 redesign, presentation-only (same IPC/props/callbacks): - Hero header now rides the shared PageHeader primitive (as=h2, title/subtitle + edged actions cluster; 添加 MCP is the primary CTA). - Market/installed tabs move onto the shared underline TabsList/TabsTrigger primitive with count pills, matching the skills tab-row rhythm; the search field pins to the tab row's right edge. - Empty + no-results states use the shared EmptyState primitive. - Installed server status is exception-only (#651): a healthy/expected server shows no status color — its label rides plain muted text and a neutral dot; only a real 连接失败 raises a destructive Chip + colored dot. - The 把 Maka 连接到你的工作环境 banner's pictogram cluster becomes labeled library-icon tiles (Terminal/Plug/Globe), and the per-template market marks drop their hand-drawn brand SVGs for the catalog text mark on the existing brand-tinted tile (de-inlines all 6 inline in this file — Task A). --- apps/desktop/src/renderer/mcp-page.tsx | 217 +++++++++--------- .../src/renderer/styles/module-pages/mcp.css | 134 +++++------ 2 files changed, 160 insertions(+), 191 deletions(-) diff --git a/apps/desktop/src/renderer/mcp-page.tsx b/apps/desktop/src/renderer/mcp-page.tsx index 3c826b378a5..d8543db891a 100644 --- a/apps/desktop/src/renderer/mcp-page.tsx +++ b/apps/desktop/src/renderer/mcp-page.tsx @@ -7,11 +7,17 @@ import { DialogContent, DialogHeader, DialogRoot, + EmptyState, Input, InputGroup, InputGroupAddon, InputGroupInput, + PageHeader, SettingsSwitch as Switch, + TabsList, + TabsPanel, + TabsRoot, + TabsTrigger, Textarea, useMountedRef, useToast, @@ -252,21 +258,23 @@ export function McpPage() { return (
-
-
-

MCP

-

连接外部应用、数据与服务,为 Maka 安全地扩展新工具。

-
-
- - - -
- + + + + + + } + />
@@ -275,33 +283,26 @@ export function McpPage() { 从精选模板开始,或添加任意 stdio、Streamable HTTP 与 SSE server。
-
-
- - + setActiveTab(value as 'market' | 'installed')}> +
+ + 市场 {MCP_CATALOG.length} + 已安装 {entries.length} + + + + setQuery(event.currentTarget.value)} placeholder="搜索 MCP…" aria-label="搜索 MCP" /> +
- - - setQuery(event.currentTarget.value)} placeholder="搜索 MCP…" aria-label="搜索 MCP" /> - -
-
- {busy === 'load' ? ( -
正在读取 MCP 配置…
- ) : activeTab === 'market' ? ( - marketEntries.length > 0 ? ( + + {marketEntries.length > 0 ? (
{marketEntries.map((entry) => ( ))}
- ) : setQuery('')} /> - ) : entries.length === 0 ? ( -
-
- ) : installedEntries.length > 0 ? ( -
    - {installedEntries.map(([serverId, server]) => ( - void toggle(serverId, server, enabled)} - onEdit={() => openEdit(serverId, server)} - onTest={() => void testServer(serverId)} - onRemove={() => void remove(serverId)} - /> - ))} -
- ) : setQuery('')} />} -
+ ) : ( + setQuery('') }} + extraClassName="maka-mcp-empty" + /> + )} + + + + {busy === 'load' ? ( +
正在读取 MCP 配置…
+ ) : entries.length === 0 ? ( + setActiveTab('market') }} + extraClassName="maka-mcp-empty" + /> + ) : installedEntries.length > 0 ? ( +
    + {installedEntries.map(([serverId, server]) => ( + void toggle(serverId, server, enabled)} + onEdit={() => openEdit(serverId, server)} + onTest={() => void testServer(serverId)} + onRemove={() => void remove(serverId)} + /> + ))} +
+ ) : ( + setQuery('') }} + extraClassName="maka-mcp-empty" + /> + )} +
+
{editor && ( @@ -373,7 +397,7 @@ function McpCatalogCard(props: { return (
{props.entry.name} @@ -408,33 +432,6 @@ function McpCatalogCard(props: { ); } -function McpBrandMark({ entry }: { entry: McpCatalogEntry }) { - if (entry.id === 'slack') return ( - - ); - if (entry.id === 'line') return ( - - ); - if (entry.id === 'google-calendar') return ( - - ); - if (entry.id === 'figma') return ( - - ); - if (entry.id === 'vercel') return ; - if (entry.id === 'supabase') return ( - - ); - return {entry.mark}; -} - function McpServerRow(props: { serverId: string; server: McpServerConfig; @@ -451,9 +448,17 @@ function McpServerRow(props: { return (
  • -
    - ); -} - function McpEditorDialog(props: { state: Exclude; saving: boolean; @@ -623,12 +617,15 @@ function delay(ms: number): Promise { return new Promise((resolve) => window.setTimeout(resolve, ms)); } -function presentStatus(status: McpServerStatus | undefined, enabled: boolean): { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'destructive' } { - if (!enabled || status?.state === 'disabled') return { label: '已停用', tone: 'neutral' }; - if (!status || status.state === 'disconnected') return { label: '未连接', tone: 'neutral' }; - if (status.state === 'connecting') return { label: '连接中', tone: 'info' }; - if (status.state === 'connected') return { label: `${status.toolCount} 个工具`, tone: 'success' }; - return { label: '连接失败', tone: 'destructive' }; +// `exception` marks the states that earn a toned Chip + colored status dot +// (status-color restraint #651). 已停用 / 未连接 / 连接中 / 已连接 are all +// expected states and stay neutral; only 连接失败 raises the destructive tone. +function presentStatus(status: McpServerStatus | undefined, enabled: boolean): { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'destructive'; exception: boolean } { + if (!enabled || status?.state === 'disabled') return { label: '已停用', tone: 'neutral', exception: false }; + if (!status || status.state === 'disconnected') return { label: '未连接', tone: 'neutral', exception: false }; + if (status.state === 'connecting') return { label: '连接中', tone: 'info', exception: false }; + if (status.state === 'connected') return { label: `${status.toolCount} 个工具`, tone: 'success', exception: false }; + return { label: '连接失败', tone: 'destructive', exception: true }; } function exampleJson(): string { diff --git a/apps/desktop/src/renderer/styles/module-pages/mcp.css b/apps/desktop/src/renderer/styles/module-pages/mcp.css index 73ad9ba6ce6..bebbd56541a 100644 --- a/apps/desktop/src/renderer/styles/module-pages/mcp.css +++ b/apps/desktop/src/renderer/styles/module-pages/mcp.css @@ -3,10 +3,6 @@ align-content: start; } -.maka-mcp-header { - align-items: center; -} - .maka-mcp-workspace { width: min(100%, 900px); min-height: 0; @@ -51,29 +47,44 @@ } .maka-mcp-hero-signal { - min-width: 240px; display: flex; align-items: center; justify-content: flex-end; - color: var(--foreground-secondary); + gap: var(--space-3); } -.maka-mcp-hero-signal svg { - width: 38px; - height: 38px; - padding: var(--space-2); +/* Labeled pictogram cluster — the same "framed glyph + caption" tiles the + skills featured banner uses, so the two extension pages read as siblings. + Icons ride the @maka/ui library (Task A); no inline SVG here. */ +.maka-mcp-hero-signal span { + width: 76px; + display: inline-flex; + flex-direction: column; + align-items: center; + gap: var(--space-1-5); + padding: var(--space-3) var(--space-2); border: var(--border-width-hairline) solid var(--border); border-radius: var(--radius-control); background: var(--background); + color: var(--foreground-secondary); } -.maka-mcp-hero-signal span { - width: 24px; - height: 1px; - background: var(--border); +.maka-mcp-hero-signal svg { + width: 22px; + height: 22px; +} + +.maka-mcp-hero-signal small { + color: var(--muted-foreground); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-medium); + line-height: var(--leading-tight); } -.maka-mcp-controls { +/* Tab row rhythm mirrors the skills page (.maka-skill-tabs-bar): an + underline TabsList on the left, the search field pinned to the right, + so the two extension pages read as siblings. */ +.maka-mcp-tabs-bar { display: flex; align-items: center; justify-content: space-between; @@ -83,46 +94,26 @@ .maka-mcp-tabs { display: flex; - align-items: center; gap: var(--space-4); + border-bottom: 0; } -.maka-mcp-tabs button { +.maka-mcp-tab { position: relative; - min-height: 36px; + height: 32px; display: inline-flex; align-items: center; gap: var(--space-1); padding: 0 1px; border: 0; + border-radius: 0; background: transparent; color: var(--muted-foreground); - font: inherit; font-size: var(--font-size-ui); font-weight: var(--font-weight-medium); } -.maka-mcp-tabs button::after { - position: absolute; - right: 0; - bottom: -1px; - left: 0; - height: 2px; - border-radius: var(--radius-pill); - background: transparent; - content: ''; -} - -.maka-mcp-tabs button[data-active="true"] { - color: var(--foreground); - font-weight: var(--font-weight-semibold); -} - -.maka-mcp-tabs button[data-active="true"]::after { - background: var(--foreground); -} - -.maka-mcp-tabs button span { +.maka-mcp-tab span { min-width: 16px; height: 16px; display: inline-flex; @@ -130,15 +121,18 @@ justify-content: center; padding-inline: var(--space-1); border-radius: var(--radius-pill); - background: var(--state-hover-bg); + background: oklch(from var(--foreground) l c h / 0.06); + color: var(--muted-foreground); font-size: var(--font-size-caption); + font-weight: var(--font-weight-semibold); font-variant-numeric: tabular-nums; } .maka-mcp-search { width: min(300px, 38vw); - height: var(--h-control-md); + height: var(--h-control-sm); margin-bottom: var(--space-2); + flex: 0 0 auto; } .maka-mcp-tab-panel { @@ -190,16 +184,6 @@ letter-spacing: var(--tracking-normal); } -.maka-mcp-market-icon svg { - width: 27px; - height: 27px; -} - -.maka-mcp-market-icon[data-brand="figma"] svg { - width: 18px; - height: 28px; -} - .maka-mcp-market-icon[data-brand="line"] span { font-size: var(--font-size-caption); } @@ -369,10 +353,16 @@ background: var(--muted-foreground); } -.maka-mcp-status-dot[data-tone="success"] { background: var(--success); } -.maka-mcp-status-dot[data-tone="info"] { background: var(--info); } +/* Status-color restraint (#651): the dot stays neutral for every expected + state; only a genuine failure (data-tone="destructive") raises color. */ .maka-mcp-status-dot[data-tone="destructive"] { background: var(--destructive); } +.maka-mcp-server-state { + color: var(--muted-foreground); + font-size: var(--font-size-caption); + font-variant-numeric: tabular-nums; +} + .maka-mcp-server-identity { min-width: 0; display: grid; @@ -461,36 +451,22 @@ white-space: pre-wrap; } -.maka-mcp-empty, +/* The empty / no-results panels ride the shared EmptyState primitive; this + class is only a light layout override on top of `.maka-empty-state`. */ +.maka-mcp-empty { + align-self: start; +} + .maka-mcp-loading { min-height: 240px; display: grid; place-items: center; align-content: center; - gap: var(--space-2); padding: var(--space-8); color: var(--muted-foreground); text-align: center; } -.maka-mcp-empty > svg { - width: 32px; - height: 32px; - margin-bottom: var(--space-2); - color: var(--foreground-secondary); -} - -.maka-mcp-empty strong { - color: var(--foreground); - font-size: var(--font-size-heading); -} - -.maka-mcp-empty span { - max-width: 52ch; - font-size: var(--font-size-ui); - line-height: var(--leading-normal); -} - .maka-mcp-editor-dialog { width: min(92vw, var(--maka-chat-measure)); grid-template-rows: auto auto minmax(0, 1fr); @@ -622,17 +598,13 @@ } @media (max-width: 760px) { - .maka-mcp-header, - .maka-mcp-controls { - align-items: stretch; - } - .maka-mcp-hero-signal { display: none; } - .maka-mcp-controls { - display: grid; + .maka-mcp-tabs-bar { + flex-wrap: wrap; + align-items: stretch; } .maka-mcp-search { From cebb8a41658d0f07088e4d623d4e55f6fb98edd7 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 18 Jul 2026 19:53:02 +0800 Subject: [PATCH 3/4] feat(icons): govern the inline-SVG ban and route menu glyphs through the funnel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-drawn inline SVG is an anti-pattern — generic UI glyphs must ride the @maka/ui/icons lucide funnel so they share one stroke, sizing seam and family. - menu.tsx: the checkbox + radio ItemIndicator glyphs drop their inline check for from ../icons.js (same precedent as model-picker's ItemIndicator), keeping the lucide funnel intact. - icon-governance-contract: a new pin machine-walks packages/ui/src and apps/desktop/src/renderer (the same tree-walk the repo-wide OverlayScrollbars ban uses) and fails on any literal in a .tsx outside an audited brand-mark allowlist (bot-brand-logo.tsx, provider-brand-marks.tsx). The allowlist is guarded against rot; new offenders fail with a message pointing at @maka/ui/icons. icons.tsx needs no exemption — it is a pure lucide re-export with zero literals. --- .../icon-governance-contract.test.ts | 59 +++++++++++++++++++ packages/ui/src/primitives/menu.tsx | 32 +--------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts index c4aabd34880..f6c85f1b3c4 100644 --- a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts +++ b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts @@ -86,6 +86,25 @@ const STROKE_EXCEPTION_FILES = new Set([ resolve(REPO_ROOT, 'apps/desktop/src/renderer/settings/provider-brand-marks.tsx'), ]); +// Governance: inline hand-drawn `` literals are an anti-pattern — +// generic UI glyphs must ride the @maka/ui/icons lucide funnel so they share +// one stroke, one sizing seam, and one family. The ONLY files allowed to hold +// a raw `` are the vendored *brand* marks (each audited below); they carry +// official multi-color logo geometry that no generic icon can stand in for: +// - packages/ui/src/bot-brand-logo.tsx — Telegram/Feishu/WeCom/… channel +// brand logos (the icon-set-no-direct-lucide contract pins each provider +// to a local SVG component, so this file MUST keep inline SVG). +// - apps/desktop/src/renderer/settings/provider-brand-marks.tsx — the LLM +// provider brand marks (SiliconCloud, Ollama, xAI, …); every path here is +// byte-provenance-pinned above to an upstream brand package. +// packages/ui/src/icons.tsx is NOT listed: it is a pure lucide re-export with +// zero `` literals, so it needs no exemption. Any NEW raw `` under +// the two source trees must fail this suite and move to @maka/ui/icons. +const INLINE_SVG_ALLOWLIST = new Set([ + resolve(REPO_ROOT, 'packages/ui/src/bot-brand-logo.tsx'), + resolve(REPO_ROOT, 'apps/desktop/src/renderer/settings/provider-brand-marks.tsx'), +]); + async function walkTsx(dir: string): Promise { const out: string[] = []; const entries = await readdir(dir, { withFileTypes: true }); @@ -128,6 +147,46 @@ describe('icon + typography governance contract', () => { ); }); + it('bans inline hand-drawn in .tsx outside the brand-mark allowlist', async () => { + // Machine-walk both source trees the same way the repo-wide + // OverlayScrollbars ban walks its workspaces — any .tsx that isn't a + // vendored brand mark must route generic glyphs through @maka/ui/icons + // instead of hand-drawing an . + const dirs = [ + resolve(REPO_ROOT, 'apps/desktop/src/renderer'), + resolve(REPO_ROOT, 'packages/ui/src'), + ]; + const offenders: string[] = []; + const seenAllowlisted = new Set(); + for (const dir of dirs) { + for (const file of await walkTsx(dir)) { + const src = await readFile(file, 'utf8'); + const hasInlineSvg = /]/.test(src); + if (INLINE_SVG_ALLOWLIST.has(file)) { + if (hasInlineSvg) seenAllowlisted.add(file); + continue; + } + if (hasInlineSvg) offenders.push(rel(file)); + } + } + assert.deepEqual( + offenders, + [], + 'Inline hand-drawn is an anti-pattern — replace it with a semantically-correct ' + + 'icon from @maka/ui/icons (add the re-export to packages/ui/src/icons.tsx if missing). ' + + `Offending files:\n ${offenders.join('\n ')}`, + ); + // Guard the allowlist against rot: if a listed brand-mark file no longer + // holds inline SVG, drop it from INLINE_SVG_ALLOWLIST rather than leaving a + // stale exemption that would silently re-permit a future offender. + for (const file of INLINE_SVG_ALLOWLIST) { + assert.ok( + seenAllowlisted.has(file), + `${rel(file)} is allowlisted for inline SVG but no longer contains any — remove it from INLINE_SVG_ALLOWLIST`, + ); + } + }); + it('session-sidebar-nav imports exactly the decided semantic icon set from ./icons.js', async () => { const src = await readFile(SIDEBAR_NAV_FILE, 'utf8'); const importMatch = src.match(/import\s*\{([^}]*)\}\s*from\s*'\.\/icons\.js'/); diff --git a/packages/ui/src/primitives/menu.tsx b/packages/ui/src/primitives/menu.tsx index f9c4fee9895..4267b4a2752 100644 --- a/packages/ui/src/primitives/menu.tsx +++ b/packages/ui/src/primitives/menu.tsx @@ -2,7 +2,7 @@ import { Menu as MenuPrimitive } from "@base-ui/react/menu"; import { cn } from "../utils.js"; -import { ChevronRightIcon } from "../icons.js"; +import { Check, ChevronRightIcon } from "../icons.js"; import type * as React from "react"; export const MenuCreateHandle: typeof MenuPrimitive.createHandle = @@ -163,20 +163,7 @@ export function MenuCheckboxItem({ ) : ( <> - + {children} @@ -206,20 +193,7 @@ export function MenuRadioItem({ {...props} > - + {children} From 0c964081e231a5efc924f5b1ebd7916dc7981fb6 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 18 Jul 2026 19:55:06 +0800 Subject: [PATCH 4/4] chore(audit): cover module-mcp in the row-alignment auditor The MCP module page now has a visual-smoke fixture; add it to the audit-alignment FIXTURES so the row-alignment governance tool sweeps the polished header, tab row, market grid and installed list too. --- scripts/audit-alignment.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/audit-alignment.mjs b/scripts/audit-alignment.mjs index babe9dec303..b20a1f043b1 100644 --- a/scripts/audit-alignment.mjs +++ b/scripts/audit-alignment.mjs @@ -17,6 +17,7 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const DESKTOP_DIR = join(ROOT, 'apps', 'desktop'); const FIXTURES = [ 'module-skills', + 'module-mcp', 'module-daily-review', 'plan-reminders', 'settings-general',