我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Cindy 桌面端旧设置输入主题兼容实战:SettingsTextInput 局部 alias 与 Tier-1 双层边界

Cindy 桌面端旧设置输入主题兼容实战:SettingsTextInput 局部 alias 与 Tier-1 双层边界 【免费下载链接】cindyConsider it done. The open-source AI agent that works out of the box · 想到就能做到。开源、开箱即用的 AI Agent。项目地址https://gitcode.com/GitHub_Trending/cind/cindy点击查看免费下载在 Cindy 桌面端把SettingsTextInput升格为标准Input的过程中最容易出问题的地方在于用户此前手写覆盖过settings-input-*的本地主题是否会自动改变外观是否正确反而是次要问题。本文以 验收记录 为主线结合 SettingsTextInput 实现、标准输入控件、Token 默认值 与 回归测试讲清这套兼容层的作用域设计、验证方法和证据边界。读完后你可以判断一次输入框迁移是否会破坏主题并复现从临时主题文件到 React 控件、再到 registry 解析的完整验证链路。导读这次改动真正要保护什么Cindy 的设计系统里存在两套并行的颜色来源一套是供给所有基础组件共用的 Tier-1 slot--text-primary、--border-default、--text-placeholder等另一套是历史设置页私有的域 alias--settings-input-text、--settings-input-border等。当设置页输入框被升格为通用Input时如果直接让Input继续读域 alias就等于把设置页的私有决定泄漏成全局默认如果直接删掉域 alias又会静默改掉用户已保存的本地主题。本批改动选择的路线是通用Input绑 Tier-1旧设置输入通过一个薄封装保留局部 alias。验收记录记录了这条路线在真实 Desktop 构建中的测量结果与证据边界仓库源码则给出了它在类名合并、Token 解析和主题加载期归一化三个层面的确切实现。一、问题模型一次输入框迁移为何会波及用户主题1.1 两套 token 的分工ui/input.tsx的字段外观由两条常量串决定注释里把判据写得很明确// apps/desktop/src/renderer/components/ui/input.tsx const FIELD_CHROME text-[var(--text-primary)] placeholder:text-[var(--text-placeholder)] border border-[var(--border-default)] focus:border-[var(--text-tertiary-stone)] focus:ring-2 focus:ring-[var(--focus-ring-soft)]; const ERROR_CHROME border-[var(--error-border)] focus:border-[var(--error-fg)] focus:ring-[var(--error-fg)];源码注释同时给出了这条判据的理由primitive 不继承域 alias防止设置页私有决定泄漏成全局默认这几个 alias 的默认值本来就 forward-resolve 到同一批 slot11 个内置主题与外部主题导入 allowlist 均未 override 它们所以默认外观逐值不变变的只是override 面。这一点在设计决策日志中被单独补申报过border / text / placeholder / focus-border 从--settings-input-*改绑 Tier-1与ui/buttonsecondary 的 G5 判据同源旧 token ID 一个未删兼容红线未破。详见 设计决策日志。1.2 旧 alias 的默认值与归属colors.ts中这五个 settings 输入相关 token 的注册值如下域 aliasLight / Dark 默认最终解析到settings-input-bgvar(--surface-card-ivory)卡片象牙底settings-input-bordervar(--border-default)Tier-1 边框settings-input-border-focusvar(--text-tertiary-stone)Tier-1 焦点边框settings-input-textvar(--text-primary)Tier-1 正文色settings-input-placeholdervar(--text-placeholder)Tier-1 占位色对应源码见 colors.ts注册语句紧随其后。注意这里的默认值是var(...)形式的间接引用而不是固定色号因此resolveThemeValue在解析时需要做多跳展开测试里的resolved()就是这么做的并设置了 12 层深度上限防止 alias 环。1.3 「导出型本地主题」是唯一可见的分裂面设计决策日志把风险量化得很具体只有在用户持有「新建本地副本」这类导出型本地主题时才可见。导出会把 colorRegistry 里的全部 token 解析成字面量写进 JSONtheme-service.ts 的exportThemeColors遍历整个 registry而resolveThemeValue优先读本地主题字面量// apps/desktop/src/renderer/themes/theme-service.ts export function resolveThemeValue(theme: Theme, id: ColorIdentifier): ColorValue | null { const override theme.colors[id]; if (override ! undefined) return override; const currentDefault colorRegistry.resolveDefault(id, theme.type); if (currentDefault ! null) return currentDefault; if (theme.type dark) return colorRegistry.resolveDefault(id, light); return null; }解析优先级因此是本地主题字面量 → registry 当前默认 → 暗色回退亮色默认。一旦某个导出型主题把settings-input-border固定成字面量这类主题修改border-default时标准输入框会跟着变而仍读域 alias 的旧消费者不会变——这条断层线在 DS-4 之前就存在本批只是把 6 个调用点挪到了「人多的一侧」并把整族收口登记为后续待办见 design-inventory.md 中desktop.settings行的下一动作。二、兼容层的实现一个只有 17 行的薄封装2.1 封装体本身// apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx const LEGACY_INPUT_CHROME text-[var(--settings-input-text)] placeholder:text-[var(--settings-input-placeholder)] border-[var(--settings-input-border)] focus:border-[var(--settings-input-border-focus)]; export function SettingsTextInput({ inputClassName, ...props }: InputProps) { return Input {...props} inputClassName{cn(LEGACY_INPUT_CHROME, inputClassName)} /; }它做了三件很克制的事完全复用标准 Input 的几何与行为尺寸档、胶囊圆角、secret眼睛按钮、mono、trailing、disabled全都来自ui/input只补四项 chrome文字、placeholder、边框、focus 边框不覆盖底色底色仍由surfaceprop 决定默认elevated白面板场景显式传surfaceivory保留inputClassName透传让调用方还能追加自己的类名。源码注释里还写了一条明确的使用规则新建通用界面直接使用ui/input只有在迁移已有 settings 域输入时才保留此封装。2.2 合并顺序决定了「错误态优先」封装的巧妙之处不在类名内容而在它被插入cn()的位置。Input内层控件的类名顺序是className{cn( w-full rounded-full outline-none transition-colors, sizeStyle.box, sizeStyle.paddingLeft, /* ... */ FIELD_CHROME, SURFACE_STYLES[surface], inputClassName, // ← SettingsTextInput 的旧 alias 在这里 error ERROR_CHROME, // ← 错误态在其后赢下同组冲突 disabled cursor-not-allowed opacity-60, )}cn()基于clsx 定制过的tailwind-merge见 lib/utils.ts同组工具类后写覆盖先写。于是border-[var(--settings-input-border)]与border-[var(--error-border)]冲突时错误态生效focus:border-[...]同理而text-前景色没被错误态覆盖仍保留旧 alias。这正是验收记录里「错误态边框与 focus 环优先于局部兼容样式」的代码依据也被单独写成了一条测试。2.3 不透传底层细节的边界封装只重新导出了尺寸与表面类型避免调用方绕过标准件自造档位export type { InputSize as SettingsTextInputSize, InputSurface as SettingsTextInputSurface, } from /components/ui/input;三档高度为 32/36/40pxs/m/l与DESIGN.md §4的 Inputs Forms 规格一致胶囊圆角、--surface-elevated底色、--focus-ring-soft焦点环、禁用态 60% 不透明。规范正文见 DESIGN.md其中也明确写了「Existing settings inputs retain their local theme contract throughSettingsTextInput」以及「Do not promote these legacy overrides into global semantic slots or rewrite user theme files」。三、四个数字输入框兼容层的真实消费者验收记录提到「原 SettingsTextInput 消费者与 AgentResource / Collaboration 的四个数字框共用同一标准组件」。这四个框在源码中都能定位位置字段取值约束提交时机AgentResourceSection.tsxmaxConcurrentCommands0..64MAX_CONCURRENT_CAPonBlur唯一提交点CollaborationSection.tsxworkerSoftLimit1..workerHardLimitonChange即时 clampCollaborationSection.tsxworkerHardLimitworkerSoftLimit..20onChange即时 clampCollaborationSection.tsxworkerIdleReleaseMinutes0..120取整onChange即时 clamp这几个调用点值得注意的细节尺寸统一用sizemd36px配合classNamew-24 shrink-0固定宽度属于典型「标签 提示 数字框」三列行布局AgentResource 的提交语义最严onBlur里先 trim、再用/^\d$/校验整数、超过 64 直接放弃、与现值相同不写盘最后才persist。注释说明了原因DESIGN §14.3规定设置页单行编辑器不得用 Enter 提交且 clamp 会绕过 main 侧的硬拒语义所以非法值一律「拒绝且不改写」Collaboration 三项是即时 clampNumber(text) || 1这类写法会把 NaN/空串兜底成下界避免输入过程中出现NaN落库这个maxConcurrentCommands框同时也是disabled的唯一使用者——三档预设写盘在途时置灰原文提到的「禁用 opacity 0.6」在这里可见。渲染测试对它们除了保留原有交互断言还额外钉住了 alias 消费// AgentResourceSectionRendering.test.tsx // DS-4 前数字框消费过这些局部主题键迁标准件不能丢掉用户覆盖。 expect(numberInput.className).toContain(text-[var(--settings-input-text)]); expect(numberInput.className).toContain(border-[var(--settings-input-border)]); expect(numberInput.className).toContain(focus:border-[var(--settings-input-border-focus)]);CollaborationSectionRendering.test.tsx对三个框做了同样的断言。这类断言的价值在于它检测的是「class 还在、但控件不再消费」这类回归而不是单纯的快照。四、旧主题文件加载期归一化placeholder 的特殊处理settings-input-placeholder是本族里的例外它不只被兼容层消费还会在加载期被改写内存态。原因写在 local-themes-normalize.ts 头部注释里--text-placeholder是后引入的统一 slot更早创建的本地主题快照只冻结了四个 per-surface placeholder key缺新 slot 时旧 override 会优先于新默认导致同一主题里 chat / ask / settings / plan-action-fb 四个输入面占位色不一致。归一化逻辑的要点触发条件text-placeholder缺失种子优先级settings-input-placeholder优先历史上它才是「淡到读着像空」的那一档其次才是其它 per-surface key动作播种出text-placeholder并丢弃四个旧 per-surface key时机与副作用在 renderer 侧mapWireTheme中执行local-themes.ts不改写磁盘 JSON、幂等重复 bootstrap 结果一致。这一点直接影响了验收测试的构造其中「旧文件缺新 placeholder slot」这一组用例就是把磁盘上只写了settings-input-*的主题作为输入验证归一化后控件仍能拿到正确的占位色。五、自动验证从临时主题文件到真实 React 控件inputThemeCompatibility.test.tsx是本批的核心回归测试。它不满足于「组件渲染没报错」而是把整条链路打通真实临时主题文件 → main loader → renderer bootstrap / normalize → 实际 React 控件的 Token 消费表达式 → 生产 registry 解析。5.1 链路是怎么接起来的// 1) 把 homedir 指到临时沙箱写入真实主题文件 const file path.join(sandbox.home, .cindy, themes, input-compat.json); fs.writeFileSync(file, bytes); // 2) 走 main 侧 loader const payload loadLocalThemesSync(); vi.stubGlobal(electronAPI, { localThemes: { listSync: () payload } }); // 3) renderer 侧 bootstrap内部会做 normalize bootstrapLocalThemesSync();之后测试从渲染结果里读出input元素的 className用正则抠出var(--xxx)的 token id再通过生产resolveThemeValue多跳解析出最终色值function consumed(field: HTMLInputElement, prefix: string, theme: Theme): string { const token [...field.classList].find((item) item.startsWith(${prefix}[var(--)); const id token?.match(/var\(--([\w-])\)/)?.[1]; if (!id) throw new Error(Missing ${prefix} color binding: ${field.className}); return resolved(theme, id); }被测的四项绑定是显式声明的避免测试与实现各说各话const BINDINGS [ [text-, text-primary, settings-input-text], [border-, border-default, settings-input-border], [focus:border-, text-tertiary-stone, settings-input-border-focus], [placeholder:text-, text-placeholder, settings-input-placeholder], ] as const;5.2 四组用例矩阵测试对 light / dark 两种模式各跑四组输入覆盖验收记录里点到的所有场景用例构造的 colors验证意图仅全局覆盖SEMANTICTier-1 全改通用 Input 跟着变兼容设置输入仍读旧 alias旧局部覆盖与全局覆盖并存SEMANTIC LEGACY证明作用域同屏两套值互不串味旧文件缺新 placeholder slot只给LEGACY验证加载期归一化后占位色仍可取完整本地副本含局部覆盖exportThemeColors(builtin) SEMANTIC LEGACY模拟「新建本地副本」导出型主题的最坏情况每种模式、每组用例的断言形式是一致的expect(consumed(legacy, prefix, theme)).toBe(resolved(theme, alias)); expect(consumed(general, prefix, theme)).toBe(resolved(theme, semantic));即兼容设置输入必须解析到旧 alias 的值通用 Input 必须解析到 Tier-1 的值。测试还额外断言两件事JSON.stringify(colors)与输入完全一致不改写传入对象以及重新读取磁盘文件字节不变。5.3 另外两条独立断言无局部覆盖时逐值相同遍历builtinThemes的全部 11 个内置主题default-light / atom-one-light / solarized-light / cindy-light / cindy-dark / default-dark / eclipse / one-dark-pro / github-dark / monokai-pro / material-ocean-hc见 registry.ts对四项绑定逐一比较封装与标准件解析值必须相等。这是「默认外观不变」的机器化证据。局部兼容不能盖掉错误态渲染SettingsTextInput error /断言边框解析到error-border、focus 边框解析到error-fg同时text-仍是旧 alias。这对应ERROR_CHROME后置的类名合并顺序。5.4 这条测试明确不声称什么测试文件里有一句关键注释jsdom 不负责 Tailwind/CSS 变量计算此处只是「从真实 React 控件读取消费表达式通过生产 registry 解析到值」补齐「文件还在但控件不再消费」的回归边界浏览器最终 computed style 另由真实 Desktop 验证不能把此测试冒充实机证据。这个分层在本批验收记录里被重复强调了一次。复现方式在仓库根目录执行pnpm --dir apps/desktop exec vitest run \ src/renderer/components/ui/__tests__/inputThemeCompatibility.test.tsx需要node 22.12、pnpm 10.7 11见根 package.json。六、真实 Desktop 构建内的组件对照jsdom 只能证明「控件消费了哪个 token」不能证明「浏览器最终画出了什么颜色」。因此本批在独立 dev 沙箱中做了受控对照包装启动与desktop-whoami均返回 ready / MATCH通过 CDP 在该构建内挂载实际的Input/SettingsTextInput用生产 Tailwind 与 ThemeService 渲染读取getComputedStyle含 placeholder 伪元素并采集 Light/Dark 截图。对照组左侧是仍保持 Tier-1 默认的通用Input等同于修复前直接 re-export 的效果右侧是局部兼容封装同屏故意使用不同的全局与局部覆盖以证明作用域。原文特别声明这些数值是验证输入不是新设计裁决。模式项目通用 Input兼容设置输入Light文字 RGB35,69,103101,67,33Light边框 RGB86,120,154135,101,67Lightfocus 边框 RGB52,86,120169,135,101Lightplaceholder RGB120,154,188170,136,102Dark文字 RGB170,204,238238,203,170Dark边框 RGB136,170,204187,153,119Darkfocus 边框 RGB187,221,255255,221,187Darkplaceholder RGB120,154,188170,136,102从这张表可以读出三点四项在两列都不同说明局部覆盖确实生效且没有外溢——如果封装失效右侧会退化成左侧的值Light 与 Dark 的偏移方向互补例Light 文字从偏蓝走到偏棕Dark 从偏蓝走到暖米说明两列读的是两套独立配色而不是同一套值被浏览器做了色彩管理placeholder 在两模式下两列的值相同120,154,188 与 170,136,102 各出现两次是符合预期的本次验证输入对占位色只设了一档不随模式分叉。同时记录的不变量两模式错误边框保持生产error token禁用 opacity 均为0.6对应disabled cursor-not-allowed opacity-60focus 环仍为原来的65,124,221RGB即#417CDD、宽度2px。这两项分别对应--focus-ring与focus:ring-2与colors.ts中focus-ring的注册值一致注意输入框自身绑的是focus-ring-soft50% alpha而读数取自评分标准里「原来的 65,124,221」这一环色基准。两张截图均已目检。七、证据边界与待交接事项原文档对证据边界写得非常克制这部分必须完整保留这是实际 Desktop 构建中的受控组件样例非 SSR、非手绘不是登录后的完整设置页端到端验证。新沙箱没有同意登录协议本次未代用户同意。完整设置页以及全部内置主题的实机检查未执行。没有宣称全局/局部同时覆盖时零视觉变化本批有意恢复设置域旧覆盖无局部覆盖的默认值由自动测试逐值核对即第五章第 3 条断言。截图与 CDP 复现/测量脚本随桌面执行记录本地交付栅格文件不入仓。当时尚未创建 PR 或上传附件提交 PR 时需补真实可访问的附件链接此项目前不记为完成。09-04 日志提出的跨 surface alias 全族收敛没有在本批执行。本批只处理已经迁移的既有输入框通用标准件保持 Tier-1旧局部 override 不提升成全局 semantic。最后这条边界对应一个可以自查的判据如果某处改动把--settings-input-*从SettingsTextInput挪进了FIELD_CHROME或某个全局 semantic slot那它就超出了本批合同如果只是新增了SettingsTextInput的使用方则仍在合同内。八、采集时的文件指纹与核对方式原记录用 SHA-256 固定采集时的实际文件内容避免把基点 commit 误当成采集版本基点5fc5881101753aba47922d089f3b3246e9b9ffce加本批次未提交修复文件SHA-256components/ui/input.tsx88527dd4fb758d9ff7c1fe425f43d67bcc4770abb79f329ad913f95bfd53dba8components/settings/SettingsTextInput.tsxfc25cad5aec84abd783904138d88448ec350795c15dd6e2b233c77832c088a57components/settings/AgentResourceSection.tsx6f1a9700998f06e69aabbc33f8f78a3b574ad0d138acff9a584d8664f931e909components/settings/CollaborationSection.tsx49ce0ba642e6d079b8d609843908279797f95d08ae16a63b6c261983c783d208仓库内这四个文件位于apps/desktop/src/renderer/下。核对当前工作树是否仍与采集版本一致cd apps/desktop/src/renderer sha256sum components/ui/input.tsx \ components/settings/SettingsTextInput.tsx \ components/settings/AgentResourceSection.tsx \ components/settings/CollaborationSection.tsx需要提醒的是指纹只说明「文件字节是否相同」不说明行为是否相同。如果哈希已变应回到第五章的自动测试与第六章的实机对照重新取证而不是以哈希不同就直接判定回归。九、迁移与维护清单把这份验收记录沉淀成可复用的规则大致是下面几条新建界面用ui/input不要为新页面引入SettingsTextInput后者是给已有 settings 域调用点用的兼容通道。迁移已有的 settings 输入时保留封装并且不要顺手把settings-input-*写进FIELD_CHROME。错误态永远由ERROR_CHROME后置覆盖任何自定义 chrome 都要插在inputClassName位置之前。不要改写用户主题文件。加载期归一化只改内存态、幂等、磁盘字节不变这是明确的兼容红线。不要删除旧 token ID。默认值可以继续 forward-resolve 到 Tier-1但 ID 一旦删除持有旧覆盖的用户主题会直接失效。验证要分层jsdom 断言「消费了哪个 token」真实构建断言「最终 computed style」两者不可互相替代。跨 surface 的 alias 全族收敛要整族一次做不按 surface 零敲——这是本批明确挂起、留给后续批次的部分。结语这次改动的技术含量不在封装本身而在边界哪一层该绑 Tier-1、哪一层保留域 alias、哪一层允许用户覆盖、哪一层只做内存归一化。SettingsTextInput用 17 行代码把这条边界固定在类名层面测试用「临时主题文件 → 控件消费表达式 → registry 解析」把边界变成可回归的断言实机构建则用getComputedStyle把边界变成可测量的 RGB 读数。三者叠加才构成一份能被后续批次安全引用的兼容证据——也解释了为什么原记录会把「没有宣称什么」写得和「验证了什么」一样详细。赞分享【免费下载链接】cindyConsider it done. The open-source AI agent that works out of the box · 想到就能做到。开源、开箱即用的 AI Agent。项目地址https://gitcode.com/GitHub_Trending/cind/cindy点击查看免费下载相关推荐3步跑通 MediaCrawler多平台数据采集实用指南3步跑通 MediaCrawler多平台数据采集实用指南 想批量拿到小红书笔记和评论做趋势分析或者一次性收齐抖音、B站的视频数据吗MediaCrawler网页爬虫数据分析Cindy 插件面板panel.html完全指南双形态承载、cindy-ghost 协议通道与主题注入机制Cindy 插件面板panel.html完全指南双形态承载、cindy ghost 协议通道与主题注入机制 本文以 Cindy 桌面端随包 Skill c深入 Cindy Desktop「仅运行脚本」自动化cindy-script/1 JSONL 协议与 Python 客户端实战深入 Cindy Desktop「仅运行脚本」自动化cindy script/1 JSONL 协议与 Python 客户端实战 Cindy Desktop 的上一篇ComfyUI-KJNodes 实战指南用自定义节点解决批量处理、显存瓶颈与模型编译三大卡点下一篇一次搞懂 Jasmine 漫画浏览器跨平台漫画阅读从入门到顺手创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表