桌面设计系统
本文档介绍了 Electron 桌面应用(apps/desktop)的规范。在添加任何组件、叠加层或样式之前,请先阅读本内容。核心原则为:一个功能对应一个来源,优先使用标记而非字面值,采用扁平结构而非嵌套结构。如果需要直接使用原始颜色、一次性阴影效果、定制按钮,或是为控件硬编码 px-* 单位——请立即停止,因为系统中早已存在对应的原生元素。
本文件定义了视觉表现与交互行为的规范。关于架构、状态管理、解析器、传输机制及测试规则,请参阅 AGENTS.md。
本文档包含两类内容,其维护方式各不相同:
- 设计原则(如扁平化、意图明确性、反馈机制、动画效果、取消操作等)是长期有效的。无论组件如何更替,这些原则始终适用。
- 命名规范(如标记、
Button的不同变体、原生元素名称等)代表了设计系统当前的 API 接口。这类内容会与代码一同维护:一旦修改了任何原生元素、标记或变体,必须在同一个提交中更新本文档中的对应条目——若此处出现过时的名称,就会像过时的类型定义一样构成缺陷。
当规范与代码之间存在冲突时,应修正错误的一方,而非在调用处临时绕过规则。
设计原则
-
扁平化设计,无嵌套框。 不允许卡片套卡片,面板内部也不设置分隔边框。应通过空白间距与细线条进行组合,绝不可使用多层嵌套的圆角框。
-
悬浮面板的无边框浮起效果。 叠加层应依托
shadow-nous效果与--stroke-nous细边框实现浮起,而非厚边框框定。面板内部结构可适度使用符号化细线条。 -
每个功能对应一个基础组件。 一个
Button、一组控制变体、一个SearchField、一个Loader、一个ErrorState——应直接使用这些标准组件,而非自行创建分支。 -
使用符号化值而非硬编码值。 应引用 CSS 变量(如
--ui-*、--shadow-nous、--theme-*),组件中绝不可直接使用十六进制值或临时指定的 rgba 值。 -
样式内置于基础组件中。 各种变体与尺寸应自行定义内边距、圆角、颜色及边框样式。调用方只需传递
variant/size参数,而非通过className重新指定这些属性。 -
先体现意图,再实现自动化。 应首先展示有用的操作选项与预览内容,切勿仅因工具生成了某些结果就自动打开面板、切换焦点或进行导航。
-
即时反馈机制。 直接操作应首先更新界面显示,后续再通过网络或磁盘持久化机制同步数据;若操作失败,需能清晰地回退到之前的状态。
- 聊天界面是主要操作区域。 对话记录与内容编辑器始终处于核心位置,而工具、预览窗口、文件、审核功能以及终端则用于辅助对话进行。
- 页面则是持久性的内容载体。 聊天、技能、消息传递及成果展示等功能均保留在主界面中。切勿将某个特定产品功能隐藏在无关的页面里。
- 路由覆盖层用于处理简短任务。 设置、控制中心、定时任务、用户资料、智能体以及星图等功能会以
OverlayView卡片的形式呈现,关闭后便会返回到之前的界面。模型/会话选择器及对话框位于当前界面之上,不属于导航层级结构。 - 面板则是用于处理具体任务的上下文环境。 预览窗口、文件、审核功能以及终端始终与当前任务绑定在一起。只要底层工具的设计初衷是保持状态持久化,那么即便这些组件被暂时隐藏或切换到其他聊天内容,其状态依然会保留。
- 一个操作,一个主界面。 一个命令可能具备键盘快捷键、调色板及可视化操作入口,但它们都会触发相同的操作并产生一致的状态。切勿根据不同的入口点而改变其行为逻辑。
- 项目负责管理自身的工作空间目录。 请通过“侧边栏 → 项目”来管理本地文件夹和工作树,无需再引入针对单次会话或右侧边栏的独立文件夹选择机制。
导航系统必须保持上下文一致性。后台会话结束、工具处理结果返回或项目刷新时,虽然可能会更新徽章信息及缓存数据,但绝不能替换当前显示的对话记录或抢占焦点。
界面类型与层级结构
浮动面板(基础Dialog类型、路由覆盖层、启动/安装/更新界面、模型选择器、引导界面、提示词覆盖层以及通知窗口)均采用以下机制:
shadow-nous /* downward-weighted, layered contact→ambient falloff */
border-(--stroke-nous) /* currentColor hairline, theme-adaptive */
这两者均为 src/styles.css 中的 CSS 变量——只需在一处进行调整,所有相关元素都会自动继承该设置。
请勿为每个叠加层单独添加 shadow-[…] 或 border-(--ui-stroke-secondary) 这类临时性样式;若需更改阴影效果,应直接修改对应的变量值。
菜单和弹出框会使用统一的 shadow-md 与 --ui-stroke-secondary 基础样式。可拖动的控件则会采用基于变量的虚线目标及局部模糊效果。这些均为语义化的表面样式类,而非允许在调用位置自行创建阴影或边框样式的许可。
玻璃态效果
无论在浅色还是深色主题下,玻璃态效果的默认参数均为29% 的透明度,且仅适用于侧边栏区域。淡入淡出效果默认为无,以确保内容列和文本保持不透明状态。系统原有的磨砂效果则遵循对应平台或主题的默认设置。用户明确保存的配置将优先生效;调整默认值时绝不能覆盖用户的现有选择。apps/shared/src/translucency.ts 这一共享解析模块负责为渲染器及 Electron 的首个窗口绘制功能设定这些默认参数。
边框与颜色变量
| Token | 用途 |
|---|---|
--ui-stroke-primary…quaternary | 线条粗细,数值越大线条越细 |
--ui-stroke-tertiary | 面板内分隔线/列表边框线以及转录内容中所有带边框区域的默认线条样式 |
--stroke-nous | 叠加式线条(需与 shadow-nous 一起使用) |
--ui-text-primary / -secondary / -tertiary | 文本层级颜色 |
--ui-bg-quaternary | 按钮的柔和填充色(用于次要按钮) |
--ui-widget-surface-background | 内嵌聊天组件的填充色(对应 WIDGET_SHELL_CLASS) |
--chrome-action-hover | 静音控制元素的悬停填充色 |
--theme-primary, --ui-accent | 品牌色/强调色 |
切勿直接硬编码 border-gray-*、bg-white、text-black 等样式值。在 BrandMark 中使用的白色背景是唯一被允许的固定背景色(因为品牌标识需要固定的背景)。
按钮 —— 单一组件
所有按钮相关逻辑均位于 src/components/ui/button.tsx 文件中。只需选择 variant 和 size 参数即可,无需传入 h-*、px-*、py-* 类型的尺寸参数,也不需指定图标大小。
可用变体包括: default(主色调按钮)、destructive、secondary(柔和填充色的次要按钮——即非主色调的默认样式)、outline(透明背景加1像素内凹边框,无填充色和阴影)、ghost、link、text(无边框的静音内联按钮,如“取消”、“清除”)、textStrong(加粗下划线的内联按钮,用于触发操作,如“更改”、“打开日志”)。
尺寸选项: default、xs、sm、lg、inline(紧贴对齐、无边框——适用于嵌入在标题或句子中的按钮;可替代 h-auto px-0 py-0)、micro(用于状态栏/表格页脚),以及图标系列 icon / icon-xs / icon-sm / icon-lg / icon-titlebar。
仅当悬停能提供新信息时才显示工具提示。 <Tip> 旨在帮助用户发现新内容,而非为每个图标都添加说明。需考虑:悬停是否揭示了用户当前无法看到或推断出的信息?若否,则无需添加提示;可通过 aria-label 确保无障碍体验。
当相关操作内容(或快捷键/截断的路径/主机名/其他细节)尚未显示在屏幕上时,应给未标注标签的图标添加提示——例如工具栏/标题栏/状态栏中的图标、TipKeybindLabel 快捷键、所有权标识,以及未标注标签的图标网格。
以下情况不应添加提示:
- 菜单触发器(如烤肉串图标/“⋯”/
ActionsMenu/DropdownMenuTrigger)——其交互意图即为“打开菜单”,相关操作描述已在菜单中体现。切勿添加诸如“${row title} 的操作”/“项目操作”/“操作”之类的提示。 - 关闭/关闭按钮(X图标)——该图标本身即为标签(仅需
aria-label)。 - 其可见标签已明确说明功能的内容(如“点击以……”,重复相同表述的文字,或计时器标签上重复显示“正在运行”)。
切勿在按钮上使用原生 HTML 的 title= 属性——它不会进行样式美化,还会导致操作系统延迟约 500 毫秒,且与主题化的提示样式冲突。只要仍有元素使用 title=,src/components/ui/__tests__/no-native-title.test.ts 测试就会失败。
工具提示显示时机。 悬停操作并不等同于点击——当光标在移动到其他位置时经过触发点,也会触发相应动作。Tip 会在首次显示前等待 200 毫秒,以避免连续触发时出现闪烁痕迹。一旦工具提示打开,页面即处于“预热”状态:随后 300 毫秒内的下一个触发点会立即被激活。冷却计时从工具提示关闭时开始计算,因此稍后再悬停需重新等待。关闭操作是即时完成的。OverflowTip 则会遵循其独立的延迟规则(在滚动浏览时列表标题不可出现拖尾现象)。
斜杠行描述。 自动补全行应保持单行显示并使用省略号,但在将鼠标悬停在斜杠行的任意位置时,应在共享的主题工具提示中完整显示该条目的描述。该工具提示的尺寸需根据窗口大小自动调整,并预留碰撞缓冲区及支持换行;同时不得干扰行选择功能。目录项和补全内容的生成方应保留作者提供的完整描述。
模型搜索。 模型过滤器及其高亮标签会将连字符、句点、下划线和空格视为等效符号。标记内的原始标签拼写应保持不变。对于命令面板等其他界面,共享的高亮功能仍会按字面显示;只有当用户明确启用时才会改变显示方式。模型标识符搜索不使用词典拼写检查功能。
工具提示中的键绑定提示。 对于绑定有可重新配置热键的按钮,应使用 <TipKeybindLabel actionId="..." /> ——该标签会读取国际化后的标签文本以及 $bindings 中当前的键组合。仅当标签内容依赖于上下文时(例如“显示”/“隐藏”),才需传递 text={...} 参数。绝不可硬编码键组合,应始终使用 useKeybindHint 或 TipKeybindLabel。
备注:
- 文本按钮为正方形形状(无圆角),其尺寸由内边距与行高决定(无固定高度)。仅有图标按钮会采用统一的4像素圆角。
- SVG元素会继承
size-3.5的样式(在xs尺寸下则为size-3),无需重新设置图标大小。 - 当按钮需要以链接或Slot的形式呈现时,可使用
asChild属性实现多态渲染。
徽章组件 — 单一组件
位于src/components/ui/badge.tsx文件中。提供多种变体:default(带主色调阴影)、muted、warn、destructive、outline、solid(实心填充——图标角点也会计入尺寸)。尺寸选项包括:default、xs、overlay(标题栏中的图标数量也会影响尺寸)。
表单控件
controlVariants(位于src/components/ui/control.ts)是Input/Textarea/SelectTrigger等控件的通用形状基础,新的文本输入控件均基于此构建。SearchField— 无边框,聚焦时显示下划线,宽度自动调整。这是唯一的搜索输入控件,不可创建带边框的搜索框,也不应将其包裹在带边框的面板中。当列表为空时,其搜索字段会隐藏。SegmentedControl— 用于处理少量互斥选项的场景(如颜色模式、工具调用显示方式、使用周期等),可替代传统的单选按钮组或药丸状按钮行。Switch(尺寸为xs) — 简约设计,配有aria-label属性,没有带边框的文本包裹层。
布局
- 边距: 页面侧边距可使用
PAGE_INSET_X(位于src/app/layout-constants.ts中);若需将子元素挤至边缘,则使用PAGE_INSET_NEG_X。请勿在页面中直接硬编码px-6/px-8这类值。 - 主页/详情页叠加层: 采用
OverlaySplitLayout结合OverlaySidebar/OverlayMain的结构。日程、个人资料等功能均可基于此实现,无需重新构建标题栏界面。 - 行结构: 标签、描述和操作等内容所在的行应使用
ListRow(定义于primitives.tsx中)。这类行采用扁平布局、左对齐方式,且不会为每行设置额外缩进,以避免与顶部标题冲突。 - 行与行之间无需添加分隔线,除非列表确实需要;此时建议使用间距来替代。若必须添加分隔线,也应仅使用单条
--ui-stroke-tertiary颜色的细线。
面板标题栏
位于顶部的面板会延伸至系统原生标题栏区域。其标签栏仍保留在自身区域内,因此标签切换、焦点切换以及分割边界均会遵循统一的几何布局。而底部的面板则保留独立的标题区域。空白标题区域可用于移动窗口;标签和操作按钮仍不可拖动,且系统会为现有的“交通灯”状态指示及窗口控制叠加层预留相应空间。
左侧区域用于显示侧边栏、设置项、布局编辑器以及 HUD 控制按钮。翻转页面后,右侧侧边栏的切换按钮会出现在右侧;触觉反馈功能则仍保留在设置选项中。按下 Cmd 键(MacOS 上为 Ctrl 键),400 毫秒后目标标签栏状态点上方会显示小型编号,且不会改变标签栏的宽度。提示信息同样遵循与编号快捷键相同的绑定规则及悬停/焦点区域识别逻辑。
粘性用户消息会通过不透明的聊天界面遮挡滚动内容,包括其上方的空白区域。请使用 data-glass-opaque 属性,以防止 Glass 清除该遮罩效果;同时不要设置渐变或背景模糊效果。
反馈状态以及空状态/错误状态/加载状态
- 加载状态:使用
Loader(位于src/components/ui/loader.tsx),通过动画呈现数学曲线或 ASCII 图形(长时间运算时会显示lemniscate-bloom效果)。切勿直接显示“Loading…”这样的文字。 - 错误状态:采用
ErrorState结合标准化的ErrorIcon(无背景色块),以便快速区分 React 组件边界内的错误、对话框内的错误以及启动失败提示。可通过传递节点信息来设置标题和描述,这样 Radix 的DialogTitle/Description组件就能被调用,从而实现无障碍功能。 - 日志显示:使用
LogView——无背景色,仅有细边框,内边距紧凑,字体为小型等宽字体。所有需要展示原始日志的地方都会采用此组件。 - 空状态:普通页面内容区域使用
EmptyState;带有图标和操作按钮的叠加式主/详情页空状态则使用PanelEmpty。无需自行实现第三种居中显示的空状态组件。 - 确认操作:仅通过
ConfirmDialog来询问用户“您确定吗?”。该对话框打开时会自动聚焦在“确认”选项上,因此按Enter键即可确认,按Esc键则取消操作。它还负责处理从待处理状态到已完成状态再到关闭状态的流程,以及显示内联错误信息——调用方只需传递一个异步的onConfirm函数即可,无需其他参数。第三种退出方式(例如在“删除工作树”选项旁添加“从侧边栏移除”)则放在唯一的secondaryAction插槽中。绝不要使用window.confirm,因为它是一个没有样式且会阻塞操作的 Chromium 弹窗。如果希望将确认结果直接显示在当前界面而非弹出对话框,可在src/store/confirm.ts中调用confirm()函数,该函数会通过壳层中的单一ConfirmHost组件来渲染相同的确认界面——这与notify()处理通知的机制类似。
聊天、工具及启动界面
- 转录组件与编辑器均基于
@assistant-ui/react构建。应扩展src/components/assistant-ui以及src/app/chat/composer中现有的组件,切勿为单个功能单独创建新的 Markdown、消息、工具调用或审批渲染组件。 - 内联小部件——即以面板形式呈现供用户查看或操作的工具结果(如说明内容、文件卡片等),会使用
WIDGET_SHELL_CLASS样式类(位于src/components/chat/widget-shell.ts中):这些小部件具有统一的半径,填充色由--ui-widget-surface-background参数决定,且没有边框。其操作按钮位于面板外部、面板下方。不得为单个小部件单独设置半径或填充色。 - 转录内容中的带边框区域(如表格、分隔线、标注框、附件等)应使用
--ui-stroke-tertiary样式。不可使用border-border,因为这是整个应用默认的边框样式,在文本背景上会显得过于醒目。 - 编辑器中的交互式指令标签在鼠标悬停时会显示其对应的操作。当鼠标从标签移至悬浮按钮时,该操作会保持可见状态500毫秒;若同时移开鼠标,则操作会被取消。
- 工具结果可提供用于打开预览的内联操作,但不得自动展开功能栏。
- 除待办事项外,编辑器中的状态组默认处于折叠状态。进度更新以及队列的暂停/继续功能会保留用户之前设定的显示选项。错误提示栏会紧贴内容堆叠的上边缘,不会出现空白边距。文件链接和预览链接则始终显示在内容堆叠的底部,位于队列及所有状态组之下。
- 安装、引导、连接、启动失败以及重新认证虽属于不同的状态,但使用了相同的视觉元素。在统一外观设计时,需保留这些状态各自的恢复逻辑。
- 需尊重
AppShell的叠加层级规则。永久性的终端/内容层、路由叠加层、对话框以及启动界面不得通过随意设定的 z-index 值来相互竞争。应优先在styles.css中指定相应的层级——例如--z-modal-backdrop/--z-modal/--z-modal-popover、--z-over-modal(用于提示信息、工具提示及命令界面)、--z-over-modal-content,以及--z-switcher-backdrop/--z-switcher;此外还有启动流程相关的层级--z-connecting→--z-onboarding→--z-setup→--z-crash。而在同一个组件内部进行层叠时,使用普通的z-10/z-20仍然是可行的。
图标与品牌标识
- Tabler 是默认的组件/主题集。请从
src/lib/icons.ts中导入其精选的别名及iconSize尺寸设置,切勿在功能代码中直接导入图标包。 Codicon用于表示紧凑型编辑器、工具及状态相关的符号。请使用src/components/ui/codicon.tsx,并在需要 Tabler 形状组件的地方调用codiconIcon()函数。- 应根据语义场景选择合适的符号,并重复使用现有的图标来表示相应操作。不得引入第三套图标集,也不应在同一个控件组中混用不同风格。
BrandMark(位于src/components/brand-mark.tsx)是品牌专属图形——即白色背景上带有柔和圆角的nous-girl标志,其浅色和深色版本外观一致。该图形已取代了更新页面、新手引导及关于页面中零散使用的 Sparkles 图形。请在展示品牌核心形象或重要内容的场景中使用它,不要再引入装饰性的星星或闪光图标。
动画效果
- 当其他应用获得焦点时,可见窗口会持续播放动画;而隐藏/最小化的窗口以及未激活的面板则可能会暂停动画,背景轮询功能仍会受焦点状态控制。
- 动态整数计数功能在
src/components/ui/diff-count.tsx中使用了AnimatedInt组件。该组件通过弹簧机制直接更新 DOM,无需逐帧进行 React 渲染。 - 过渡效果流畅且响应迅速(控件操作仅需约 100 毫秒)。对于除淡入淡出之外的其他过渡效果,请尊重用户的
prefers-reduced-motion设置。 - 有序的退出动画(例如引导流程中的“矩阵式”淡出效果)会逐个元素依次播放,之后再整体完成表面元素的动画——外部容器的淡出效果会被延迟,以避免掩盖内部元素的动画。切勿让全局淡出效果抢先于细节动画呈现。
- 动画始终跟随状态变化,而绝不会因等待状态更新而延迟。选择项、拖动目标、取消操作以及按下反馈都会在当前帧立即显示。
- 在正在进行的交互操作中,切勿使用
transition-all来动画化布局几何属性。应为相应属性指定名称,避免在元素移动时触发背景模糊效果的重新绘制,并在出现性能问题时先取消动画。
直接操作与性能优化
即使在处理长文本、多个面板或实时流媒体等高负载场景时,应用也应保持即时响应的体验。设计时应以此为目标。
- 直接操作优先进行绘制;若失败,则通过持久化机制进行协调处理,并能清晰地回滚到之前的状态。
- 将交互反馈的开销降至最低:高频访问的状态应保持本地化或仅做简单衍生,避免嵌入复杂的结构中;指针操作则应在每一帧内完成合并处理。
- 每个放置区域仅有一个视觉负责人,且无论是在不同文件、会话、标签页还是窗格之间,放置目标都遵循统一的交互规范。重叠的目标会自动聚焦到当前活跃的目标上,而非形成层叠效果。
- 容错性更强的几何结构优于追求像素级精确度的触发机制;边缘操作应靠近其实际边界执行,而非集中在中心区域。
- 开销较大的带状态表面在隐藏时仍会保持挂载状态。可见性并非决定其生命周期的因素。
用真实的内容来证明速度。一个快速的空状态演示无法体现处理长文本或繁忙终端时的性能表现。
键盘操作与取消功能
- 键盘控制权随焦点变化而转移。处于焦点状态的界面将拥有对应的按键控制权;shell快捷键不得抢占终端或编辑器的键绑定。
- 聚焦“会话”侧边栏时,会保留上一个活跃聊天的视觉高亮状态。各会话面板仍可通过亮度差异区分,侧边栏导航不得使聊天内容失去色彩,也不得将高亮焦点转移到隐藏的主标签页上。
- 应通过共享层注册全局快捷键,而非使用临时的监听器。
- 一个取消手势仅执行单一功能:要么取消当前的交互操作,要么关闭最顶层的可关闭界面——绝不能同时执行这两项操作,更不得影响其下方的控件。
- 即使后续的清理工作是异步的,UI层面的取消操作仍是同步的:覆盖层、光标以及待处理的手势状态会立即清除。
- 那些刻意设计为不可关闭的流程(如安装/引导流程、具有破坏性后果的操作确认界面)必须明确说明这一点。
国际化
- 所有面向用户的字符串都必须经过
useI18n()函数处理(位于src/i18n/context.tsx文件中)。JSX 中不得出现硬编码的字符串。 - 需同时更新所有语言版本——包括
en、ja、zh和zh-hant。如果在en.ts文件中修改了字符串而未同步更新其他语言版本,即属于功能退化(如标点符号错位、标签过时)。这四种语言版本的标点符号和语调必须保持一致。
状态管理(TypeScript)
详细的状态规范记载在受限访问的 AGENTS.md 文件中。可视化代码的编写需遵循以下基本原则:
- 共享/跨组件状态 → 使用小型纳米存储器,而非通过 prop 传递。每个功能模块负责管理自身的数据原子,而共享的原子则存放在
src/store中。 - 渲染组件通过
useStore订阅状态,非渲染操作则通过$atom.get()读取数据。 - 当组件无需显示完整数据时,可订阅派生出的粗粒度事实,而非高频更新的原始数据原子。
- 将动作模块与专用钩子集中放置,每个钩子仅负责单一任务。
- 将持久化功能与对应的数据原子放在一起,以保持路由结构的简洁性。
- 公共属性优先使用
interface定义,而非直接扩展 React 原生类型(如React.ComponentProps<'button'>、Omit<…>)。
可用性设计
- 在组件底层(如按钮、下拉菜单/选择框)直接添加
cursor-pointer属性,无需在每个调用处手动硬编码。 - 全局焦点环会重置;标题栏操作不存在“活动背景”状态。
- 按下
Esc键可关闭所有可关闭的覆盖层/对话框(安装界面和引导界面除外);关闭按钮应使用 X 图标,而非文字“Close”。
添加新功能前的检查清单
- 是否应直接复用基础组件(如
Button、SearchField、SegmentedControl、ListRow、Loader、ErrorState、LogView、ConfirmDialog),而非自行创建? - 是否应使用令牌(如
--ui-*、shadow-nous、--stroke-nous)来实现零原始颜色值/一次性阴影效果? - 是否禁止通过
className覆盖基础组件的内边距、尺寸、圆角或边框样式? - 仅在悬停时能提供新信息的场景才显示提示(不使用嵌套提示或菜单触发式提示;对于需要用户自行发现的未标注边框,应使用
<Tip>元素并添加aria-label)? - 按钮是否无需内置
title=属性? - 对带有提示的按钮,其键绑定提示是否使用了
useKeybindHint/TipKeybindLabel? - 叠加层是否采用
shadow-nous和border-(--stroke-nous)样式,而不设置固定边框? - 设计风格是否为扁平化——不使用嵌套卡片结构,也不添加不必要的行分隔符?
- 是否禁止通过背景事件实现自动导航、焦点抢占或面板切换?
- 直接操作功能能否立即生效,并在失败时干净地撤销操作?
- 热交互设计是否避免了广泛的监听、布局性能问题以及
transition-all动画? - 键盘控制逻辑及单次按
Esc键的响应行为是否正确? - 所有新增或修改的字符串是否都已更新到四种语言版本中?
-
cursor-pointer指针样式、焦点环以及通过Esc键关闭的功能是否正常工作? - 当修改了基础组件、令牌或其变体时,该文件中对应的命名契约条目是否同步得到更新?