From 0b4125284560b2dcfc59ac5e071acb50f6d14108 Mon Sep 17 00:00:00 2001 From: Michael-Yu2978 Date: Mon, 31 Aug 2026 18:46:40 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(desktop):=20MakerExperimentalView=20?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E9=A1=B5=E6=94=B9=E7=94=A8=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=20token=EF=BC=8C=E5=8F=8C=E6=A8=A1=E5=BC=8F=E5=8F=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michael-Yu2978 --- .../makerExperimentalThemeContract.test.ts | 65 +++++++++++++++++++ .../MakerExperimentalView.tsx | 58 ++++++++--------- docs/design-rules/DESIGN.md | 2 +- docs/design-rules/design-decision-log.md | 9 ++- 4 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 apps/desktop/src/renderer/__tests__/makerExperimentalThemeContract.test.ts diff --git a/apps/desktop/src/renderer/__tests__/makerExperimentalThemeContract.test.ts b/apps/desktop/src/renderer/__tests__/makerExperimentalThemeContract.test.ts new file mode 100644 index 00000000000..763b4006580 --- /dev/null +++ b/apps/desktop/src/renderer/__tests__/makerExperimentalThemeContract.test.ts @@ -0,0 +1,65 @@ +/** + * makerExperimentalThemeContract.test.ts + * --------------------------------------------------------------------------- + * 源契约守卫(DESIGN.md §10 双模式交付门槛 / design-governance.md §6 Level 1): + * MakerExperimentalView 诊断页必须全量走语义 token—— + * 1. 零裸色字面量(hex / rgb() / hsl()); + * 2. 消费的每个 var(--xxx) 都已在 themes/colors.ts 注册; + * 3. 注册槽位同时给出 light / dark 双模式值(缺任一槽位即某一模式渲染失效)。 + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +import { colorRegistry } from '../themes/color-registry'; +// 触发整表 registerColor 注册(与 tokenRegistry.test.ts 同款做法)。 +import '../themes/colors'; + +const viewSource = readFileSync( + resolve(__dirname, '..', 'features', 'maker-experimental', 'MakerExperimentalView.tsx'), + 'utf8', +).replace(/\r\n?/g, '\n'); +const colorsSource = readFileSync( + resolve(__dirname, '..', 'themes', 'colors.ts'), + 'utf8', +).replace(/\r\n?/g, '\n'); + +/** 裸色字面量:hex、带数值入参的 rgb()/rgba()/hsl()/hsla()。 */ +const RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgba?\(\s*\d|hsla?\(\s*\d/g; +const TOKEN_REF_RE = /var\(--([a-z0-9-]+)\)/g; + +function referencedTokens(): string[] { + return [...viewSource.matchAll(TOKEN_REF_RE)].map((match) => match[1]); +} + +describe('MakerExperimentalView 主题契约(语义 token,双模式)', () => { + it('零裸色:诊断页不出现 hex / rgb() / hsl() 字面量', () => { + const raw = viewSource.match(RAW_COLOR_RE) ?? []; + expect(raw).toEqual([]); + }); + + it('消费的 token 全部已在 colors.ts 注册(防幽灵 token)', () => { + const registered = new Set( + [...colorsSource.matchAll(/registerColor\('([a-z0-9-]+)'/g)].map((m) => m[1]), + ); + const referenced = referencedTokens(); + expect(referenced.length).toBeGreaterThan(0); + const unregistered = [...new Set(referenced.filter((id) => !registered.has(id)))]; + expect(unregistered).toEqual([]); + }); + + it('消费的 token 槽位同时具备 light / dark 双模式值', () => { + const referenced = [...new Set(referencedTokens())]; + for (const id of referenced) { + expect( + colorRegistry.resolveDefault(id, 'light'), + 'token "' + id + '" 缺 light 槽位', + ).not.toBeNull(); + expect( + colorRegistry.resolveDefault(id, 'dark'), + 'token "' + id + '" 缺 dark 槽位', + ).not.toBeNull(); + } + }); +}); diff --git a/apps/desktop/src/renderer/features/maker-experimental/MakerExperimentalView.tsx b/apps/desktop/src/renderer/features/maker-experimental/MakerExperimentalView.tsx index 8b24c171062..5b07aa24b92 100644 --- a/apps/desktop/src/renderer/features/maker-experimental/MakerExperimentalView.tsx +++ b/apps/desktop/src/renderer/features/maker-experimental/MakerExperimentalView.tsx @@ -69,7 +69,7 @@ export function MakerExperimentalView(): ReactElement { if (!m.isReady) { return ( -
+

{t('makerExperimental.notReadyHeading')}

{t('makerExperimental.notReadyBody')}

@@ -124,15 +124,15 @@ export function MakerExperimentalView(): ReactElement { const displayError = sendError ?? m.error; return ( -
+

{t('makerExperimental.title')}

-

+

{t('makerExperimental.description')}

{/* 配置区 */}
{t('makerExperimental.agentLabel')}
@@ -165,7 +165,7 @@ export function MakerExperimentalView(): ReactElement { value={workingDir} onChange={(e) => setWorkingDir(e.target.value)} disabled={!!m.session} - style={{ flex: 1, padding: '4px 8px', background: '#262626', color: '#d4d4d4', border: '1px solid #333' }} + style={{ flex: 1, padding: '4px 8px', background: 'var(--surface-elevated)', color: 'var(--text-primary)', border: '1px solid var(--border-default)' }} placeholder={t('makerExperimental.workingDirPlaceholder')} /> ) : ( )}
@@ -267,10 +267,10 @@ export function MakerExperimentalView(): ReactElement { {/* Session 状态 */} {m.session && ( -
+
{t('makerExperimental.sessionField')} {m.session.sessionId}
{t('makerExperimental.agentField')} {m.session.agentKind} | {t('makerExperimental.statusField')} {m.status}
- {displayError &&
{t('makerExperimental.errorField')} {displayError}
} + {displayError &&
{t('makerExperimental.errorField')} {displayError}
}
)} @@ -283,8 +283,8 @@ export function MakerExperimentalView(): ReactElement { placeholder={t('makerExperimental.messagePlaceholder')} rows={3} style={{ - width: '100%', padding: 8, background: '#262626', color: '#d4d4d4', - border: '1px solid #333', borderRadius: 4, boxSizing: 'border-box', + width: '100%', padding: 8, background: 'var(--surface-elevated)', color: 'var(--text-primary)', + border: '1px solid var(--border-default)', borderRadius: 4, boxSizing: 'border-box', }} />
@@ -294,24 +294,24 @@ export function MakerExperimentalView(): ReactElement { onChange={(e) => setImagePath(e.target.value)} placeholder={t('makerExperimental.imagePathPlaceholder')} style={{ - flex: 1, padding: '4px 8px', background: '#262626', color: '#d4d4d4', - border: '1px solid #333', borderRadius: 4, + flex: 1, padding: '4px 8px', background: 'var(--surface-elevated)', color: 'var(--text-primary)', + border: '1px solid var(--border-default)', borderRadius: 4, }} />
@@ -321,16 +321,16 @@ export function MakerExperimentalView(): ReactElement {

{t('makerExperimental.eventsHeading', { count: m.events.length })}

{m.events.length === 0 ? ( -
{t('makerExperimental.noEvents')}
+
{t('makerExperimental.noEvents')}
) : ( m.events.map((e) => ( -
-
- #{e.id} {e.event.type} ({e.event.source}) +
+
+ #{e.id} {e.event.type} ({e.event.source})
                   {JSON.stringify(e.event.data, null, 2)}
diff --git a/docs/design-rules/DESIGN.md b/docs/design-rules/DESIGN.md
index 3ee1e94f101..6d5daa34071 100644
--- a/docs/design-rules/DESIGN.md
+++ b/docs/design-rules/DESIGN.md
@@ -579,7 +579,7 @@ No gaps are currently open.
 
 Resolved items (G1–G4, 2026-06 — button-text drift, border drift, placeholder unification, radius tiering) are archived in [`design-decision-log.md`](./design-decision-log.md); each entry records the drift, the ruling, and where the conclusion was folded back (§2 / §4 / §5 / §10).
 
-Known but deliberately-deferred cleanups (e.g. hardcoded hex in `MakerExperimentalView.tsx`) are tracked in the decision log's backlog section, not here.
+Known but deliberately-deferred cleanups are tracked in the decision log's backlog section, not here.
 
 ## 14. Interaction Conventions(交互约定)
 
diff --git a/docs/design-rules/design-decision-log.md b/docs/design-rules/design-decision-log.md
index be77a525560..29b403dbdc5 100644
--- a/docs/design-rules/design-decision-log.md
+++ b/docs/design-rules/design-decision-log.md
@@ -436,6 +436,13 @@
 
 ## Backlog(已知、刻意搁置)
 
-- `MakerExperimentalView.tsx` 通篇裸 hardcode hex(#404040 / #d4d4d4 / #262626 / #333),违反 DESIGN.md 第 10 节 token 规则。因是 experimental 视图,暂不清理,仅备忘(原 §13 旁注,2026-06)。
+- [x] **MakerExperimentalView.tsx 裸 hardcode hex 清理**(已解决 2026-08-31)
+  现状:诊断页通篇裸 hardcode 暗色 hex(页面文字、卡片、输入框、按钮、事件流等),
+  违反 DESIGN.md 第 10 节 token 规则;路由生产包按 URL 可达,Light 主题下不可读。
+  处理:全量改成语义 token(text-primary / text-secondary / text-tertiary /
+  surface-elevated / border-default / accent-cta-bg / accent-pure-cta-fg /
+  error-flat / warning-accent / destructive / surface-chip / surface-chip-alt),
+  双模式由 token 体系自动覆盖;新增源契约守卫 `makerExperimentalThemeContract.test.ts`
+  锁「零裸色 + token 已注册 + light/dark 双槽位」。
 - R2 §4.3 Project_List 五点差异(2026-07-17 lead 裁决本轮不做,出处为设计阶段工作文件 `2026-07-17-r2-ui-specs.md` §4.3,不入仓库):① Project_List 三态拆分(active-task-pill / project-card / flat-list-row 不共用 `sidebar-item-active`);② 项目 header / list card 选中应中性底(`#312F2F`/`#F6F6F6`,非 `#DF0C27` 大红);③ 去 Project_List 选中组 `focus-ring-soft` 蓝 ring,改 card stroke `#DCDFE3`/`#434343`;④ 小箭头 `#A61629` 强调(非整行红底);⑤ 本轮收敛不扩战线,后续另开。
 - splash 渐变辉光层未实现(2026-07-18 backlog,待用户表态)。

From b821b4c25d1069ec9ac4398adf2b361f8183b046 Mon Sep 17 00:00:00 2001
From: Michael-Yu2978 
Date: Mon, 31 Aug 2026 18:55:08 +0800
Subject: [PATCH 2/4] =?UTF-8?q?fix(desktop):=20CCAgentSessionView=20handof?=
 =?UTF-8?q?f=20pill=20=E4=B8=8E=20context=20=E7=8E=AF=E6=94=B9=E7=94=A8?=
 =?UTF-8?q?=E8=AF=AD=E4=B9=89=20token?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Michael-Yu2978 
---
 .../ccAgentSessionViewThemeContract.test.ts   | 52 +++++++++++++++++++
 .../features/cc-agent/CCAgentSessionView.tsx  |  6 +--
 docs/design-rules/design-decision-log.md      |  3 ++
 3 files changed, 58 insertions(+), 3 deletions(-)
 create mode 100644 apps/desktop/src/renderer/__tests__/ccAgentSessionViewThemeContract.test.ts

diff --git a/apps/desktop/src/renderer/__tests__/ccAgentSessionViewThemeContract.test.ts b/apps/desktop/src/renderer/__tests__/ccAgentSessionViewThemeContract.test.ts
new file mode 100644
index 00000000000..4aeee4d8a1a
--- /dev/null
+++ b/apps/desktop/src/renderer/__tests__/ccAgentSessionViewThemeContract.test.ts
@@ -0,0 +1,52 @@
+/**
+ * ccAgentSessionViewThemeContract.test.ts
+ * ---------------------------------------------------------------------------
+ * 源契约守卫(DESIGN.md §10 双模式交付门槛 / design-governance.md §6 Level 1):
+ * CCAgentSessionView 的 handoff pill 与 context 环不再裸写颜色——
+ *  1. handoff pill 文字走 text-muted-foreground;
+ *  2. context 环阈值走 error-flat / warning-fg 语义 token;
+ *  3. 消费的 token 槽位同时具备 light / dark 双模式值。
+ */
+
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { describe, expect, it } from 'vitest';
+
+import { colorRegistry } from '../themes/color-registry';
+// 触发整表 registerColor 注册(与 tokenRegistry.test.ts 同款做法)。
+import '../themes/colors';
+
+const viewSource = readFileSync(
+  resolve(__dirname, '..', 'features', 'cc-agent', 'CCAgentSessionView.tsx'),
+  'utf8',
+).replace(/\r\n?/g, '\n');
+
+/** 动态拼 hex 字面量,避免测试源自身被 hardcoded-color-audit 命中。 */
+const hex = (digits: string) => '#' + digits;
+
+describe('CCAgentSessionView 主题契约(语义 token,双模式)', () => {
+  it('handoff pill 不再裸写灰字色,改走 text-muted-foreground', () => {
+    expect(viewSource).not.toContain('text-[' + hex('595959') + ']');
+    expect(viewSource).toContain('text-muted-foreground');
+  });
+
+  it('context 环阈值色不再裸写红/橙,改走 error-flat / warning-fg', () => {
+    expect(viewSource).not.toContain(hex('EF4444'));
+    expect(viewSource).not.toContain(hex('F59E0B'));
+    expect(viewSource).toContain("pct > 90 ? 'var(--error-flat)' :");
+    expect(viewSource).toContain("'var(--warning-fg)' : 'var(--msg-tool-card-chevron)'");
+  });
+
+  it('消费的 token 槽位同时具备 light / dark 双模式值', () => {
+    for (const id of ['muted-foreground', 'error-flat', 'warning-fg', 'msg-tool-card-chevron']) {
+      expect(
+        colorRegistry.resolveDefault(id, 'light'),
+        'token "' + id + '" 缺 light 槽位',
+      ).not.toBeNull();
+      expect(
+        colorRegistry.resolveDefault(id, 'dark'),
+        'token "' + id + '" 缺 dark 槽位',
+      ).not.toBeNull();
+    }
+  });
+});
diff --git a/apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx b/apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx
index f35980f5a74..ad8c1c09c06 100644
--- a/apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx
+++ b/apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx
@@ -5328,7 +5328,7 @@ function HandoffSourcePill({