桌面 UI 维护
修改桌面界面时,使用 renderer 的共享组件和设计变量,并在真实浏览器或 Electron 窗口中检查结果。类型检查和 jsdom 测试无法证明间距可读、滚动效果正常,或键盘焦点清晰可见。
Wuu 设计系统集中说明颜色、排版、间距、圆角、层级和交互规则,并提供可再生的浅深主题图版。设计变量的用途查规范,真实组件与状态的检查入口查本页。
预览真实组件
Section titled “预览真实组件”完成开发环境设置后,启动 renderer 预览服务:
cd desktopnpx vite --host 127.0.0.1使用 Vite 显示的端口打开 /dev/design-system/ 或 /dev/button-standards/ 等路径。desktop/dev 包含特定组件与状态的夹具,使用合成数据,不会复现所有产品桥接和生命周期。
新手引导有独立的 Electron 入口:
npm --prefix desktop run dev:onboarding它渲染真实的新手引导组件,但不使用产品 preload、app-server 或持久化 profile。选择不会保存,模型凭据请使用虚构值。按 Cmd+R 或 Ctrl+R 重新开始;正常退出时会删除临时 profile。这个预览检查呈现,不验证登录或设置持久化。
小球实验室(英文)使用 npm --prefix desktop run lab:mascot。修改依赖原生行为、IPC 或真实会话状态时,使用完整的 make dev 路径。临时截图放入已忽略的产物目录,提交的夹具使用合成内容。
签名的 macOS 正式发布版发现 GitHub 稳定新版后,会在侧栏底部的账户控件旁显示下载箭头。 点击开始下载,圆环显示下载进度;下载完成后变成绿色重启图标,点击后仍需确认重启。 下载失败时保留重试入口。悬停或键盘聚焦可查看版本、进度;系统和应用内的减少动态效果 设置都会关闭出现与完成时的动画。
紧凑的状态反馈参考 Motionbook 的 Micro progress button, 原始设计参考来自 withAnimationUI。 Wuu 独立绘制形状,使用共享图标与实际下载进度,没有引入参考素材或字体。 发布要求见自动更新(英文)。
设置 → 归档列出所有工作区中的归档会话。删除全部归档会先刷新归档清单,再确认准确 数量,包括被搜索或工作区筛选隐藏的会话。删除会永久移除历史记录和所属产物;干净的受管 工作树会被回收,有未提交修改的工作树会被保留。未归档的会话不会被选中,删除前已恢复 的会话也会受到服务端保护。
操作只处理确认时的清单。执行过程中显示进度,禁用重复提交和恢复按钮。部分失败后可 再次确认并重试仍在归档中的失败会话,后来新增的归档不会加入重试。取消或关闭确认框 即可保留会话。
工作区新建页面
Section titled “工作区新建页面”右侧面板的加号会创建一个可关闭的新建页面标签。选择工具后,在原位置替换该页面; 如果工具已经打开,则关闭新建页并切回已有标签。已安装扩展收在更多工具里。 通过顶部标签栏切回已打开的文件、差异或交付产物。 关闭页面会恢复之前激活的标签。新建页面的分区保持易读宽度,但从标签下方的面板 边缘缩进处开始,不在宽面板里居中,因此工具图标与标签图标对齐。
每个标签按标题宽度显示,有上限,不再平分整条标签栏。选中的标签是比纸面低一级的 浅色底块,标题使用中等字重。标签比标签栏里的图标按钮高一级,先读到内容,再读到 操作。全面板按钮不保留按下的底色,图标本身已经表明方向。
运行 npm --prefix desktop run test:e2e:workspace-new-tab,通过合成桥接检查生产 renderer
中的选择、单例复用、关闭恢复、键盘导航和扩展加载。desktop/out/workspace-new-tab-e2e/
里的截图和 results.json 覆盖浅深主题、14px/20px UI 字号及宽窄窗口;不验证真实
app-server 或浏览器引擎。
会话图片预览
Section titled “会话图片预览”查看文件或读取工具输出时返回的图片保留在聚合工具活动中。展开工具过程后才加载预览, 再点击图片可放大查看;检查图片不会拆开工具聚合或独立占据输出行。主动展示的图片产物、消息附件和 Markdown 图片仍直接显示。 PTC 和后台工具返回的结果也按同一规则处理。
消息正文和工具结果中的单图共用更宽、左对齐的预览,圆角贴合图片本身。 横向多图使用统一行高,宽度随图片比例调整;平铺和 Markdown 表格保留规整的 4:3 单元格。图片等比完整显示,不裁剪、不放大小图。
预览行在加载前预留高度,加载失败后仍保留,避免切换会话后图片加载完成时挤动文字。 较小图片或全景图下方可能留白。横向多图加载时宽度可以调整,但行高保持不变。 点击预览仍可查看原图,工作区文档中的图片继续按自然比例排版。
运行 npm --prefix desktop run test:e2e:image-layout,可在 Electron 中验证冷加载、
加载失败、缓存后的会话切换和底部跟随。几何记录与截图保存在
desktop/out/image-layout-e2e/。
图片预览按展示顺序收集当前会话中已展示的上传图片、工具结果图片和消息图片。 点击上一张、下一张按钮,或按左右方向键切换,计数器显示当前位置。 到达首尾后停止切换,只有一张图片时不显示切换控件。折叠内容或更早的历史需先展开、加载。 预览打开后图片组保持固定,流式输出不会改变当前位置。切换图片会重置为适应窗口大小、 无旋转的状态;按住 Shift 配合左右方向键可平移放大的图片。Esc 关闭预览并将焦点返回打开入口。
Markdown 表格在悬停或键盘聚焦时显示复制和展开按钮,触屏上始终可见。
复制保留表格的 Markdown 源码。宽表格支持横向滚动,边缘渐隐提示尚未显示的列。
回答中过宽的表格在窗口空间允许时可向阅读栏两侧各延伸最多 120px(环境面板打开时不延伸),
超出该范围后仍在表格内横向滚动。Markdown 列对齐(---:、:---:)会应用到表头与单元格。
展开后在与图片共用的预览外壳中阅读,沿用当前主题和字号,表格内部独立滚动。
右上角关闭按钮、Esc 和点击遮罩均可关闭预览,并将焦点返回展开入口。
从表格打开内嵌图片后,表格仍保留;关闭图片即可返回表格。
视频产物卡片会在右侧面板打开播放器,工作区视频文件使用同一个播放器。 视频不会自动播放,原生控件支持暂停、拖动进度、音量和全屏。 本地文件识别 MP4/M4V、WebM、MOV 和 OGV 容器,能否解码取决于桌面运行时。 播放失败时显示提示,并保留下载入口。交付快照继续校验完整性, 拖动视频进度使用的分段请求也不例外。
运行 npm --prefix desktop run test:e2e:artifact-preview,可在 Electron 中
用合成内容验证交付预览,不读取应用中的个人数据。
图片、视频、PDF 和粘贴的文本附件放在同一个从输入框上沿滑出的托盘里, 添加或移除都不会改变输入框尺寸。托盘只改变一次布局,上方内容通过反向补偿的 合成层动画平滑升降,输入框保持不动。从托盘移除的卡片在原位淡出, 相邻卡片滑动补位;发送或切换草稿时托盘直接清空。托盘始终只有一行, 横向滚动,边缘渐隐见“滚动边缘渐隐”。
不支持的附件类型及附件添加失败统一使用胶囊通知,不在输入框上方常驻显示; 普通对话和分屏对话保持一致。
使用 /dev/composer-attachments/ 预览,可选参数有 theme=dark、size=20、
width=420、hero、queued 和 seed。页面按钮通过真实的输入框粘贴处理
粘贴合成文件。
输入框的项目与分支控件
Section titled “输入框的项目与分支控件”新对话和已有对话的项目及 Git 分支均位于输入框下方的无边框操作行中。不在 Git 中的文件夹 只显示项目。收起右侧工具栏时,权限、上下文用量和模型选择位于这一行的右端, 其余工具入口保持隐藏。窄面板允许换行并截断过长名称,不缩小文字。 输入框不再提供手动工作树开关。
新对话的两个分组都打开紧凑菜单卡片。已有对话保留项目标签,不提供修改项目绑定的入口;
分支菜单切换当前工作树的分支,工作树内有任务运行时禁用。项目卡片与侧栏一致,先列出对话,再按侧栏顺序
列出项目;打开文件夹和新建空白工作区位于分隔线之后。分支卡片在工作树之外不需要
标题:当前检出的分支排在最前,并附未提交文件数,其后是本地默认分支,其余分支按
最近提交排序。codex/ 这类共同前缀以弱色显示,让分支自身的名称承担这一行。搜索框
同时是新分支名称:输入的名称没有对应分支时会多出新建一行;只有没有任何分支匹配时,
回车才会新建,部分匹配不会变成新分支。草稿在独立工作树中开始时,卡片标题为
起始分支,选择分支不会改动项目,也不提供新建分支。
运行 npm --prefix desktop run test:e2e:composer-worktree,会通过桌面 API 和
Go 核心,使用一次性配置、Git 夹具和本地模型服务,真正在 worktree 中开始一个
会话。它会检查指定基准版本不会改动项目的检出,并在 artifacts/composer-worktree/
中截取浅色/深色、默认/大字号、宽/窄窗口以及非 Git 状态。窗口是可见的,指针
位置可能让截图带上悬停底色。
当前工作树放在环境信息面板中,可查看基础仓库、基准提交和路径,不再向对话插入 创建提示。Agent 切换工作区时,操作记录保留在处理过程里实际调用工具的位置; 展开可查看那次调用的目标路径或错误,后续切换不会改写之前的记录。同一 E2E 还会 验证切回项目目录,并截取工具记录和当前工作树信息面板。
面板中,变更摘要和分支是状态行;提交与拉取请求跟在其后,作为一行安静的文字操作,
与行标签对齐。用户通常让 Agent 提交和推送,所以这两个操作不再单独成组。
desktop/dev/environment-panel/ 渲染该面板,其 capture.cjs 在两种主题的默认和
大字号下检查关闭按钮、行几何和待办对齐。
设置导航顶部依次为常规、外观。连接包含模型与服务、MCP 服务器,以及可用时的浏览器和手机访问;数据包含用量和归档。插件页面排在插件分组下。搜索匹配页面名称和设置关键词,也包括插件页面名称,并隐藏没有结果的分组。筛选时保留当前页面,按 Esc 清空搜索。搜索不会改变页面 ID 或插件设置快照中的完整页面列表。
所有页面共用一列按 UI 字号计算的宽度,切换页面时内容位置不变,大字号下标签和控件也不会拉开。标题栏写「设置 / 页面名」,排版与对话标题一致,页面操作位于标题栏末端;刷新类操作是图标按钮,由提示说明它做什么。页面从说明不明显作用范围的可选一行文字开始,随后就是第一个分组。分区标题是柔和墨色的安静标签;每行的设置名称使用中等字重,下方说明保持常规字重和柔和墨色。分组、服务列表、服务目录和订阅卡片使用同一种浮起的面板:--hairline 边线、面板圆角和柔和阴影,行距统一为 --settings-row-pad-block;分区标题与内容相隔 --section-heading-gap。只有标题没有说明的限制或后果,才保留行说明。单位则放进数值输入框:数字占位符保留单位,「自动」这类文字占位符单独显示。
能正常工作的行不额外说明状态。只有需要留意的状态才出现符号:缺少凭据或待决定时是警告三角,失败时是圆形标记;符号的无障碍名称和提示说明原因,含义不依赖颜色。常规包含语言、系统权限和版本等应用设置。Agent、模型和思考强度在对话输入框中选择,并为新对话记住选择。上下文管理、PTC 和执行环境默认值通过用户配置修改,不再提供单独的内置 Agent 页。Agent 页列出已安装的 Agent,每行一个开关和一个展开按钮;Wuu 找不到的 Agent 归入未安装,每行带安装操作和路径覆盖,不再在每一行重复原因。Agent 在各自的行下方展开。没有展开按钮的行保留它的占位,让末尾控件结束在同一条轴线上。标题栏只在内容滚动到下方后才显示分隔线。
Codex 桌宠属于应用外观,在外观里单独成节;常规包含语言、Git 提交署名和关于。归档分组显示项目和不带单位的会话数,年份只在不是今年时显示,恢复是以会话命名的图标按钮。
模型服务围绕“自带 API key”设计,按使用频率从上到下排列。最上面是已连接的服务,排成一组列表,默认服务带默认标记,缺少密钥或登录时显示警告。可添加的服务是更轻的图块,放在同样的底面上连成一组,标志与上方卡片的标志对齐:先是订阅登录,再是按界面语言排出的常用厂商,另有打开整个模型目录的更多服务,以及连接任意 OpenAI / Anthropic 兼容地址的自定义端点。只有完整目录按类别(订阅、服务商、中转、本地)分组,页面上的短列表不分。页面不放解释性的小字,服务的名称、标志和模型数量已经说明它是什么。连接时只需要填写密钥,端点和推荐模型来自模型目录;只有打开“设为默认模型”才会改变默认,当前默认无法使用时这个开关默认打开。点开服务卡片进入它自己的页面,顶部可返回,页面不重复列表行已经显示的套餐和余额。需要处理时(缺少密钥或登录)页面先给出连接;否则先是额度(只在有重置时间等列表行没有的信息时出现),再是模型,最后是连接。模型列表每行只有名称、与名称不同时的灰色模型 ID 和行尾开关;拖动整行或按 Alt+↑/↓ 排序,默认模型带默认标记,右键或 Shift+F10 打开的菜单里是设为默认和删除。删除模型需确认,删除默认模型时还要选择替代模型。模型较多时列表第一行是搜索,最后一行是添加模型,点开后原地变成输入框。连接(认证方式、密钥、Base URL 或登录)平铺在页面中,在原行内编辑。页面底部是删除连接,删除前需确认。Agent 页面用同一套模型列表,已安装时模型在前,否则先显示运行设置。弹窗把标志、标题和关闭按钮放在同一行,每个下拉菜单的选项文字都对齐到触发器的文字。编辑服务不会改变默认模型,只有设为默认会。厂商标志来自随应用打包的 models.dev 图标,不在运行时下载图片。
npm --prefix desktop run test:e2e:model-services 通过 Electron 和 Go 核心,使用一次性配置、凭据存储和本地模型服务驱动这些流程:连接目录中的服务而不改变默认、选择、删除并跨重新加载恢复它的模型、把自定义端点设为默认、删除服务及其密钥,并发送一个必须以新默认和已保存密钥到达的回合。截图和 evidence.json 写入 artifacts/model-services-e2e/。
通过 /dev/settings/ 预览,用 page 指定页面 ID,可选参数包括 theme=dark、size=20、lang=en、rail=(侧栏宽度)、collapsed、long 和 empty。模型服务、Agent、MCP 服务器、用量和归档数据均为示例,不会保存任何设置。
侧栏的拓展入口下分为插件和技能。目录沿用设置页的内容列,在工具栏上方用大标题标明当前分类;工具栏依次为搜索、重新加载和安装本地插件。没有侧栏导航时,目录显示带数量的插件和技能标签页;没有插件时直接打开技能。搜索会隐藏所有没有匹配项的分组。
插件按需要你做什么分组,每组是一张带边框的列表,与设置分组一致。需要处理的每一行写明插件和最需要留意的一个原因——启动失败或缺少依赖,其次是待授权的更新、内容已更改或尚未授权,以及冲突——点开进入插件页,在那里做决定。已启用和未启用的每一行包含图标、名称和一行简介,在原地开关已信任插件的开关,以及通向插件页的箭头。打开尚未授权的插件时,它申请权限就先打开插件页,不申请权限就直接授权。行里显示清单中的显示名称和简介,而不是插件 ID。技能同样分组列出,分为官方技能和你的技能;技能行末尾显示所属插件的名称(如有)和打开预览的箭头。
插件以独立页面打开,顶部有返回目录的链接。页头包含图标、名称和简介,以及开关或插件等待的决定(授权,或授权更新);拒绝、撤销授权和移除位于更多菜单中。接着是提示:为什么需要授权、有可用更新、运行错误,或缺少、冲突的插件。下面依次是清单中的详细介绍、在 Wuu 中(它添加的侧栏页面、工作区工具、设置页面、命令、技能和主题),以及以普通设置行呈现的插件设置。权限只在授权时显示。版本、指纹、路径、授权范围和 Agent 工具不出现在页面上。
通过 /dev/extensions/ 预览,可选参数包括 theme=dark、size=20、lang=en、long 和 empty。技能与插件包均为示例数据,覆盖每一种插件状态;开关和详情中的操作只改变预览里的状态。
分叉目标弹窗只提供当前目录和新的工作树两种选择。弹窗宽度与间距随已保存的 UI 字号变化;两个选项共用图标列和文字列,并预留加载状态的位置,避免文字跳动。点击弹窗外的空白或按 Escape 即可关闭,不会创建对话,焦点会回到原消息的操作按钮。分叉正在创建时暂时锁定关闭操作。
审查面板按文件夹列出变更文件,并在旁边显示当前文件的 diff;面板宽度不够同时容纳两者时,先显示列表,选择文件后再显示 diff。只有一个变更文件时直接打开它,不显示列表。diff 标题栏包含文件状态与增删行数,以及已查看、在编辑器中打开、上一个、下一个操作。
在 desktop/ 下运行 npx vite --config dev/review/vite.config.ts,打开 /dev/review/,可选参数包括 theme=dark、size=20、lang=en、width=(面板像素宽度)以及 set=one、clean、repo 或 long。改动数据是合成的。服务运行时,./node_modules/.bin/electron dev/review/capture.cjs 会把浅色/深色、默认/大字号、宽/停靠/窄面板和已打开文件等状态截图到 artifacts/review-panel/,并在 results.json 中记录每张截图的布局和溢出元素。截图需要人工检查,不是合并门禁。
失败的回合与确认
Section titled “失败的回合与确认”失败的回合只显示一张卡片:用平实的话说明发生了什么;有 HTTP 状态码时以弱化的代码显示;下一步是一个按钮;技术记录收在详情后面。凭据被拒绝、额度用尽或服务里没有所选模型时,以打开模型服务为先,旁边是重试。限流、过载、超时和网络失败提供重试。对话超出模型上下文时不提供重试,因为重放会以同样的方式失败,卡片会提示运行 /compact 或开始新对话。只有最新一个回合带操作。
不可逆或会打断工作的操作通过 ConfirmDialog.tsx 里的 confirmAction 询问。它像被替换的原生提示一样返回用户的回答,并沿用产品自己的字体、主题和文案:标题写明对象,一句话说明标题没有说清的后果,确认按钮重复动词;销毁数据的操作用 tone: "danger" 标记。不要调用 window.confirm。
Agent 的提问或引擎的批准请求是一张卡片:作为提示盖在输入框上,或在阅读流里紧挨着对应回合。巡检里的 Agent 不会拿到 ask_user,引擎批准也需要真实引擎,所以通过 /dev/question-card/ 预览正式的卡片,可选参数包括 theme=dark、size=20、lang=en、width=、inline、two、approval、long、multi 和 expires。请求均为示例数据,不会发送任何内容。
巡检整个产品
Section titled “巡检整个产品”npm --prefix desktop run test:e2e:ui-tour 使用一次性配置和主目录、示例项目与脚本化的本地模型服务,驱动真实的 Electron 应用、preload 和 Go 核心,不涉及账号或模型。它通过真实输入框生成对话(纯文本、工具调用、思考过程、被拒绝的凭据),再在由主题、界面字号和窗口宽度组成的矩阵中,逐个外观访问主要界面:主页、对话、输入栏菜单、工作区面板标签、搜索、插件、账户菜单和每个设置页。默认矩阵是浅色、深色、20px 界面字号和 720px 宽的窗口。
每个停靠点保存截图和几何审计。页面横向溢出会使运行失败;审计只记录在用的圆角、低于 4.5:1 的文字、被裁切的文字和小于 24px 的控件,供比较两个版本。运行还会走完跨界面的路径,任何一条断开都会失败:草稿在设置与对话切换后仍在,选中行跟随导航,关闭菜单或搜索后焦点回到原处,长对话在切换后保留阅读位置。
截图、audit.json 和 report.json 写入每次运行都会清空的 artifacts/ui-tour-e2e/,可用 WUU_TOUR_OUTPUT 改位置。WUU_TOUR_ONLY=settings,plugins 按子串选择停靠点,WUU_TOUR_MATRIX=dark:20:1280 替换外观,WUU_TOUR_CORE 指向另一个核心二进制,WUU_E2E_HIDDEN=true 让窗口不显示也不抢焦点。截图是供人查看的证据,不是视觉门禁;不同平台的字体也不相同。
共享字体与尺寸关系
Section titled “共享字体与尺寸关系”输入框反馈放在输入区域上方的共享阅读区,不放在发送按钮旁。主输入框和分屏都在这里保留错误、操作限制与操作进度,长文本换行并可滚动。恢复的草稿和更新的 Git 结果已表达成功,不再通过全局状态字段重复确认。
base.css定义字体、颜色、圆角、焦点和层级等基础角色;spacing.css定义间距角色、密度边界和控件最小尺寸。优先使用已有角色,不为每个组件另设常量。
鼠标点击不额外画外框。输入框只保留光标,其他控件保持静止和悬停表面。Tab,或在非文本控件上按方向键时,才显示 2px 的 --focus-ring 指示框。输入框和多行文本里的方向键不算键盘焦点切换。Chromium 在点击输入框时仍会匹配 :focus-visible,因此描边由 FocusModality.ts 写在 html[data-focus-modality] 上的状态决定,而不是单独依赖该伪类。不要为点击再写一层描边,也不要在已有边框外再套一圈。
悬停提示用于说明纯图标操作,或补充不明显的状态、限制和目标位置。不要重复已经完整显示的标签,也不要在整张卡片上挂一段说明。名称和路径使用 TruncatedText,仅在实际截断时提示。DOM 控件使用带 asChild 的 Tooltip,保留布局、选择器、事件处理和 ref;复合组件可以保留包装元素。控件的无障碍名称与描述独立于视觉提示提供。原生窗口控件和嵌入浏览器边界可以保留原生提示。
短提示使用用户设置的 UI 字号、常规字重和不透明主题底色,在视口范围内换行,紧邻触发控件且不遮住控件。原生内容占据周围空间时,自绘提示保持隐藏。同一时间只显示一个短提示;悬停等待意图明确后出现,键盘焦点也可触发,仅鼠标聚焦不会弹出。缩短后的连续悬停延迟只用于同类相邻触发点,避免从工具栏划入详情卡区域时立即弹卡。
尊重用户分别设置的 UI 与代码字号。行高随内容增长,为末尾操作和状态标记预留空间,同级标签的对齐不应随运行或未读状态改变。密度调整留白,不移除最小点击尺寸;粗指针设备使用更大的控件尺寸下限。
紧凑菜单使用 --menu-inset、--menu-item-gap 和 --menu-shell-radius。外层圆角由内层圆角加内缩距离得到,使嵌套圆角保持对应关系;面板和对话框使用各自的圆角角色。给每个有 padding 的层设置同一个圆角数值,并不能得到相同的几何关系。点击打开的浮层卡片(右键菜单、权限选择、下拉面板)的选项文字使用 --font-menu(比 --font-ui 小一档),字重 --weight-medium;分组标题和次要说明使用 --font-xs。输入栏芯片与浮层同档。输入框上方的提问卡仍用 --font-ui,那是阅读面,不是菜单。
桌面端针对对象的上下文操作(重命名、置顶、移动、复制、删除等)和标准文本编辑使用可选的公开能力 application.desktop.contextMenu。按功能划分,不按右键或按钮的触发方式划分。操作系统负责字体、间距、定位和键盘导航,菜单请求归属于当前挂载的 UI,UI 退出时关闭菜单。网页端保留共享的自绘菜单。创建入口、输入框的附件与命令、工作区和分支选择器、模型控件、面板切换继续使用自绘界面。会话操作按重命名与置顶、文件夹归属、目录与 ID 工具、删除分组;不可用的操作禁用,破坏性操作单独放在底部。
右键菜单的选项写操作,不写操作对象:写“在系统浏览器打开”,不把完整 URL 拼进去。需要展示目的地时,放在触发元素的悬浮提示或单独的次要信息里。右键菜单的尺寸由操作决定,始终留在窗口内,内容过长时省略或滚动,不横跨正文。同一时间只打开一个右键菜单;菜单打开期间不显示悬浮提示,与原生菜单一致。
公开插件主题 token 的契约范围小于所有内部 CSS 变量。暴露新 token 或建议插件作者依赖内部变量前,请查看主题参考。
SVG 图形
Section titled “SVG 图形”产品控件从 WuuIcons 导入图标。 iconArtwork.ts 中的原创 24 单位图形也供浏览器浮窗使用, 命名扩展图标和技能标记复用相同画法。第三方品牌保留其可辨识的标识。
通过 /dev/icons/ 按 12–32px 对照整个图标族,以及真实工具栏控件的浅深色、
默认与大字号和禁用状态。同一控件角色内应匹配视觉大小、线条密度和居中效果,
相同 SVG 盒子尺寸并不足够。将轮廓校准留在图形源中,不在各页面单独缩放。
大幅插图和表达数据的 SVG 保留各自的布局与有意义的几何关系。
另提供 12 个独立的莫兰迪彩色 SVG, 供需要彩色的界面按需选用。沿用同一轮廓,采用低饱和的鼠尾草绿、灰蓝、陶土粉、 燕麦色与灰紫,可在图标预览中对照明暗背景。这些是备用资产,普通控件仍使用语义前景色。
通过 /dev/three-pane/?background 预览全局背景;去掉 background 参数可检查同一张图片在三栏中的呈现,加上 empty 参数可检查空会话画布与输入框下方的底部纸带。导入与处理后的图片保留在本地桌面 profile,栅格处理在线程中完成,不随布局变化重复执行。只有主画布透出图片;菜单、输入框、编辑器和覆盖式抽屉保留主题底色。
插件页面根节点应由宿主提供画布,不要重复铺不透明底色。这样主页面、设置页和工作区页面可以共享壁纸,覆盖式视图和辅助视图仍由宿主保留实色底。通过 /dev/automation/?region=primary 预览真实自动化插件;workspace、settings、overlay 和 auxiliary 可检查其他容器。
在端口 5189 启动 Vite 后,运行 npm --prefix desktop run test:e2e:background-image(其他端口可设置 WUU_FIXTURE_ORIGIN)。它使用隔离 profile 检查导入失败、事务回滚、图片效果、持久化、跨窗口更新,以及生产 CSP 下的打包线程。实际渲染像素检查覆盖上述容器中的真实插件,包括覆盖式视图保留实色底,同时将浅深色、默认与大字号、宽窄窗口截图写入 artifacts/background-image/。截图需要另外审阅;像素和几何检查不代表视觉验收通过。
置顶、文件夹和工作区标题支持鼠标拖拽排序,顺序保存在桌面 profile 中。
侧栏分区和嵌套分组统一使用 SidebarCollapseBody。它让内容自身高度与标题间距同步过渡,在收起结束前保留列表,并阻止隐藏行获得焦点。不要通过可继承的临时高度变量或后代动画规则,让父级切换改变嵌套折叠区。
通过 /dev/sidebar-collapse/ 预览,可选查询参数包括 theme=dark、size=20 和 width=240。运行 npm --prefix desktop run test:e2e:sidebar-collapse,在 Electron 中检查嵌套折叠、反向点击、内容变化和减少动态效果的几何行为;这些检查不能代替视觉验收。
项目会话列表以现有侧栏顺序的前 5 条为基础(保留已保存的手动排序),额外包含选中、切换中、运行中和未读会话。未读变为已读的回执会让该会话继续保留 2 分钟,每个项目只保留最近读过的 3 条,位置沿用原有顺序。再次未读、移出列表或关闭项目分组时清除对应的暂时保留记录。选中、切换中、运行中和未读会话独立于这项限制,仍然进入候选列表。
“显示更多”每次按侧栏顺序新增五条尚未显示的会话,不足五条时仅显示剩余会话,全部显示后隐藏该入口。因状态已显示的会话不占用批次名额。“收起”回到上述范围,不关闭项目分组。各个范围都按实际内容占高,包括正在创建的会话,并使用侧栏共享的外层滚动区域。在项目会话上滚动会带动外层侧栏;展开历史会将后续分组向下推,显示更多和收起按钮跟在会话列表后面。预览地址加上 mode=history 可查看真实项目组件;同一个 Electron 检查覆盖 1,003 条会话的分批展开、已读变化、外层侧栏滚动、创建中的会话和动态字号变化,并将几何 JSON 与浅深色、14/20px、宽窄截图保存在 desktop/out/sidebar-collapse-e2e-* 下。
动画 token 只有一处来源:base.css 中的阶梯。--motion-fast(120ms)用于指针反馈,--motion-base(180ms)用于菜单、弹层和内容切换,--motion-slow(280ms)用于结构位移,--motion-slower(440ms)用于较大的折叠。入场使用 --ease-out,退出使用 --ease-in。过渡、入场和退出都读取阶梯中的一级或旁边的语义别名。字面时长只留给节奏类动画,例如使用 --motion-spin 的 spinner、环境循环动画,或由 JS 时钟驱动的编排;这类规则要在旁边写明减少动态效果时的行为。
入场和退出使用共享 keyframe,不要为每个界面复制一份。wuu-enter 和 wuu-exit 从被动画元素上的 --enter-x、--enter-y、--enter-scale、--enter-opacity(或对应的 --exit-* 属性)读取偏移:
.toast { --enter-y: 8px; --exit-y: -4px; animation: wuu-enter var(--motion-base) var(--ease-out) both;}
.toast.closing { animation: wuu-exit var(--motion-base) var(--ease-in) both;}它们驱动的是独立的 translate 和 scale 属性,因此界面自身的 transform(例如居中或悬停上浮)仍然生效。这些偏移属性注册为不继承,嵌套界面不会拿到父级的距离。wuu-fade-in 和 wuu-fade-out 是纯淡入淡出,wuu-pulse 是环境不透明度脉冲(--pulse-opacity),wuu-spin 是唯一的 spinner。menu-enter、content-swap-enter 和环境面板的进出场保留各自的命名角色。/dev/motion/ 夹具展示阶梯、共享 keyframe,以及使用它们的真实界面。
减少动态效果有两个来源:系统设置和应用内的“动态效果”偏好,结果只有一个。base.css 把任一来源解析为 --motion-reduced: 1,并把阶梯和固定别名归零,因此 token 驱动的动画无需组件规则即可瞬时完成。让入场元素的静止样式保持可见,由归零的时长承担减少动态效果;animation: none 还会去掉 fill,而静止样式本身隐藏的界面正靠 fill 才能显示。阶梯管不到的动画,在定义旁边用 @container style(--motion-reduced: 1) { ... } 退出。不要使用 @media (prefers-reduced-motion),它只能看到系统设置。spinner 保持转动,因为它表示正在进行的工作。
motion.ts 是唯一的 JS 桥接层。motionDurationMs 和 motionCurve 在动画开始时读取 token;在模块加载时固化的值会错过之后的偏好变化或主题覆盖。motionEasing 为逐帧循环求值 token 对应的 cubic-bezier,messageMotionTime 提供逐帧循环与 WAAPI 入场共用的文档时钟。请保持这个结构:在 cubic-bezier() token 旁边手写一个 1 - (1 - p) ** 3,两者很容易和它原本要对齐的过渡逐渐偏离。prefersReducedMotion、subscribeReducedMotion 和 useReducedMotion 与样式表读取同样的两个来源,不要直接查询媒体特性。样式表管不到的动画要显式检查它们:WAAPI、逐帧循环、scrollTo({ behavior: "smooth" }),以及 dnd-kit 的内联排序过渡和放下动画;后者由 SortableMotion.ts 接到阶梯上。退出动画期间需要保持挂载的内容使用 useExitPresence:它在退出开始时读取时长,也可以由动画结束事件提前释放。
新增动画后,在 /dev/motion/ 把 Motion 开关切到 reduce 检查一次,再在 DevTools 中模拟系统设置(Rendering > prefers-reduced-motion)检查一次,两者的效果必须一致。
会话内的程序化滚动统一走一条轨迹:ScrollGlide。每个 60fps 帧保留剩余距离的 0.85,因此速率与需要移动的距离无关;逼近过程不会反向或过冲,掉帧最多按八个基准帧补齐,最后 1px 精确落位。目标位置每帧重新读取,所以流式输出、输入框收起或迟到的重排都只会延长同一次运动,而不会重新开始;也是因此,发送后的气泡能在文档位移时保持屏幕位置不动。
该轨迹建模的是“位置 + 实时目标”,而不是剩余距离:放置阶段需要在 React commit 期间(此时尚未流逝任何帧)补偿重排,按流逝时间计算的步进在那里不会移动,会让气泡明显偏移到下一帧才被纠正。因此发送后的放置阶段以轨迹落位结束(约 350ms 覆盖 96%,随后是收敛尾段),不再使用固定截止时间;跨度更大时耗时更长,而不是甩过去。降级动效由调用方决定,直接用一次写入完成放置。
几何已经确定的动画用阶梯里的 CSS 过渡:入场、菜单、面板滑到固定尺寸、悬停底色。只有当终点在运动过程中才能确定时(滚动到仍在到达的内容),或者有别的布局变化在下面不断推动目标时,才用逐帧轨迹。这类运动不需要截止时间,也不会重新开始;用过渡表达就必须再补一次过渡去改目标,那正是这套设计要消除的接缝。刻意保持恒定的节奏不要改:摘要按每秒约十二个字符显现,本身就是设计意图,换成收敛轨迹会变成另一种动画。
会话详情折叠与滚动
Section titled “会话详情折叠与滚动”当前活跃的过程行显示最新的执行中工具调用;调用间隙保留最近一项的完成状态。较早的过程分组和已结束回合汇总展示工具活动。有工具调用时,思考内容保留在展开详情中;只有思考内容的行继续显示思考状态。紧凑行保持原有的灰色。
展开或收起工具、推理详情时,保留用户原有的滚动模式。正在跟随最新内容的会话应在高度过渡期间继续跟随;已暂停的会话保留阅读位置。滚轮、触摸、键盘滚动、拖动滚动条和文字选择优先于布局补偿。
详情内部的工具与思考按事件发生顺序排列。首次打开运行中的记录时定位最新内容, 已完成的历史记录从头显示。收起后重新展开,恢复详情内部的阅读位置和跟随/暂停 状态,包括收起期间内容增长的情况。定位在绘制前完成,展开动画结束后不再延迟 强制滚动。
发送 query 后,气泡下方会预留阅读空间。展开详情可以临时占用这部分空间,收起后恢复扣除实际回复增长或主动浏览消耗后的剩余量。留白暂时变为零,不代表回复已经填满预留区域。验收时应覆盖流式输出期间反复展开、收起,详情高度超过剩余留白,以及详情打开时切换会话的情况。
向前分页历史会在视口上方插入内容。已暂停阅读的偏移归浏览器原生 scroll anchoring 负责,因此手动补偿只在偏移仍停在发起分页时的位置才生效;在锚定之上再加一次补偿,会让消息流整体下移所插入的高度,分页一到就表现为跳变。
长会话的跟随与渲染
Section titled “长会话的跟随与渲染”切换会话复用现有的会话加载动画,在目标恢复前遮住原视图。先恢复目标的折叠状态和阅读位置,再显示内容;即使缓存的滚动偏移没有变化,也要强制布局落点附近的轮次。恢复完成前禁用编辑和发送,后一次选择会取代之前的请求。等待绘制后的几何信息稳定,但给布局阶段设置上限,避免流式输出或外部资源让会话一直被遮住。释放遮罩前先解码视口内的消息图片;恢复期间隐藏“跳到最新”等导航操作,等恢复完成后按实际滚动位置重新判断。恢复失败时结束加载并显示错误。
会话持续跟随最新内容,直到用户接管。滚轮、触摸、键盘、滚动条和文字选择会在浏览器派发滚动之前暂停跟随,因此流式输出的新片段无法把视图拉回去。向下回到最新内容时,会在一个小范围内恢复跟随,用来吸收滚动收尾期间新到的输出;向上滚动永远不会恢复跟随。“跳到最新”在点击时就恢复跟随,而不是等运动落地;轨迹逼近不断移动的底部后,交还给普通跟随。
视口外的轮次通过 content-visibility: auto 跳过布局和绘制,并保留上次渲染时的高度。每个轮次在允许跳过之前都会先完整渲染一次,因此被跳过的轮次不会退回占位高度;占位高度与真实高度不同,会在轮次最终渲染时移动阅读位置和滚动条。Chromium 要在一帧绘制之后才判断哪些被跳过的轮次进入了视口,所以 ConversationRenderWindow 会在大幅滚动自身的 scroll 事件中渲染视口附近的轮次。在帧内写入 scrollTop 的代码(例如滚动轨迹和轮次导轨)写入后也要调用它。不要读取被跳过轮次内部的几何信息:读取会强制执行跳过本来省掉的布局。非活动的缓存会话使用 content-visibility: hidden。
回复流式输出期间会变化的值(例如发送后的阅读预留)写在使用它的元素上,而不是作为可继承的自定义属性写在祖先上,否则每个已渲染的轮次都要重新计算样式。出于同样的原因,位于会话之前的外壳子元素(例如侧栏拖拽条)保持挂载并改为隐藏,而不是插入或移除。
npm --prefix desktop run test:e2e:conversation-scroll 在真实 Electron 窗口中用原生输入驱动这些行为:流式输出与滚轮输入、回到最新内容、跳到最新、暂停阅读时的输出完成和侧栏切换、侧栏切换后的大幅滚动、跳转历史 query、发送定位与交接,以及会话恢复。它会检查绘制出的像素中是否有空白带,因此默认显示窗口;设置 WUU_E2E_HIDDEN=true 可隐藏。在长会话中极快地拖动滚动条,仍可能有一帧超过 Chromium 的光栅化速度;即使所有轮次都完整渲染也是如此。
滚动条可见性
Section titled “滚动条可见性”滚动条只在其容器确实发生滚动时出现,停止滚动后淡出。悬停不会显示滚动条:阅读有限高度的工具/推理检查区时,指针本就落在区域内,把 thumb 画在文字上只是噪音——而该区域的边缘渐隐已经在提示下面还有内容。滚动内层区域也不会点亮外层祖先的滚动条。
ScrollbarReveal.ts 独占 .scrollbar-visible 这个 class,并为整个文档安装一个捕获阶段的 scroll 监听:所有滚动容器(包括之后才挂载的)都由它覆盖,不需要各组件自行接线。scrollbars.css 用 --scrollbar-ink 绘制 thumb,通过这个注册过的 <color> 自定义属性显式驱动淡入淡出,不依赖引擎对 scrollbar-color 的插值实现。滚动条槽位始终保留,因此 thumb 出现时消息流不会位移。
自行管理滚动节点的控制器需要调用 markScrollbarRevealSelfManaged() 登记:会话视口和所有自动跟随容器在流式输出、视口尺寸变化时也会程序化滚动,全局监听无法区分这些帧和用户手势;不登记的话,一次跟随就会让整个回复期间 thumb 一直亮着。终端和编辑器保留常显 thumb,因为滚动位置本身就是这些界面要展示的内容。已经用边缘渐隐表达溢出的地方,用 .scrollbar-hidden 直接去掉滚动条。
滚动边缘渐隐
Section titled “滚动边缘渐隐”scroll-fade.css为有限高度的工具/推理检查区、导航列表和横向卡片条提供按需启用的渐隐。将属性加在已有的滚动节点上:
<div className="existing-scroll-region" data-scroll-fade="compact" ref={scrollRef}> {content}</div>密集检查区使用 compact,导航列表使用空值;输入框附件托盘这类横向卡片条使用 inline,改为渐隐左右两端。消息、设置、文档等主要阅读区保留普通裁切。输入框、终端、编辑器、图片/PDF 画布以及代码块不适用。Markdown 表格在横向滚动容器上使用 inline,复制和展开按钮留在遮罩之外。固定标题、输入区和菜单应留在遮罩节点外。
该工具使用自身滚动时间线和透明度遮罩,不增加覆盖层或 React 滚动更新。只有边缘外还有内容时才渐隐,没有溢出就没有渐隐。嵌套滚动节点相互独立,每侧渐隐最多占视口一半。不支持相关特性的浏览器、减少动态效果、强制颜色和打印模式都回退为普通裁切。接入前检查已有的 animation 和 mask-image 声明,因为该工具会控制两者。
检查受影响状态
Section titled “检查受影响状态”检查浅深色主题、默认和大字号、宽窄窗口、空白和长内容,以及键盘焦点。组合检查可以同时出现的状态,例如选中、运行、未读、悬停、禁用和拖动。注意裁切、重叠、移动的点击目标,以及被隐藏操作或占位元素挤偏的标签。
滚动修改要检查短内容与溢出内容在顶部、中间和底部的表现。追加流式内容、手动上翻、收起再展开折叠区、切换会话,并调整窗口大小,确认跟随/暂停、文字选择、菜单和滚动条仍可使用。报告实际检查过的条件;一张截图或单元测试通过不代表完整的视觉验收。改动涉及共享组件或 token 时,请运行 UI 巡检,并在整个矩阵里查看截图。