- 人工智能
- AI Agent
- 即时通讯
- 后端
- 本地部署
- 语音
【免费下载链接】openclaw-cn
中文社区版OpenClaw,同原版保持定期更新,已内置钉钉、企业微信、飞书、QQ、微信以及国内网络环境优化。你的专属个人AI助手。支持所有操作系统和平台。🦞
本文围绕 macOS 客户端 Clawdbot 的菜单栏(Menu Bar)状态体系展开,系统讲解菜单栏 Critter 图标如何根据 Agent 会话活动实时切换工作状态、工作徽标(Badge)与状态行文案的完整链路:从控制通道
agent事件摄取、WorkActivityStore状态机裁决、IconState枚举建模,到图标渲染与调试覆盖机制。读完本文,你将掌握该菜单栏状态模块的完整数据流、会话优先级规则、活动类型映射与测试验证清单,可直接用于二次开发或问题排查。
一、菜单栏状态展示总览:什么会被“表面化”给用户
Clawdbot 的菜单栏(Menu Bar)承担着向用户即时反馈 Agent 工作状态的角色,其展示逻辑由三部分构成:
- 菜单栏图标(Critter 图标):以一只程序绘制的“小生物”(Critter)形象呈现当前 Agent 是否在忙碌、正在执行哪一类工作;
- 菜单第一行状态文本(Status Row):当有会话处于工作状态时,显示
<会话角色> · <活动标签>,空闲时则回落到健康(Health)状态摘要; - 菜单内的补充区块:
Nodes区块只列出设备(经node.list配对的节点),而非客户端/presence 条目;当提供商用量快照可用时,在 Context 下方会额外出现Usage(用量)区块。
值得注意的是健康状态的显示规则:工作状态激活期间健康状态被隐藏,只有全部会话都进入空闲后健康摘要才会回归。这一“非此即彼”的展示策略,避免用户被同时呈现的两类信息干扰,保证菜单栏信息永远是单一、明确的焦点。
从源码结构看,这一逻辑的载体分别位于 MenuBar.swift(菜单栏入口与状态项装配)、MenuContentView.swift(菜单内容视图)与 NodesMenu.swift(Nodes 区块渲染)。
二、会话状态模型:main 会话优先与“不摇摆”切换规则
菜单栏状态机的核心输入是会话(Session)的活动事件。事件到达时带有两个关键标识:
runId:标识一次运行(per-run),与事件序列号seq共同组成事件的唯一id(形如"\(runId)-\(seq)");sessionKey:位于事件data负载中,标识事件所属的会话。
ControlAgentEvent在 ControlChannel.swift 中定义为:
struct ControlAgentEvent: Codable, Sendable, Identifiable { var id: String { "\(self.runId)-\(self.seq)" } let runId: String let seq: Int let stream: String let ts: Double let data: [String: ClawdbotProtocol.AnyCodable] let summary: String? }其中main是“主会话”的约定键名。若某事件未携带sessionKey,则回落到"main"(见 ControlChannel.swift 的routeWorkActivity)。
会话状态裁决遵循两条核心规则:
- main 永远优先:如果
main会话处于活动状态,立即展示其状态,即使此刻有非 main 会话正在工作; - 不做中途摇摆(no flip-flop):当 main 空闲、非 main 会话活跃时,展示最近活跃的非 main 会话;状态只在“当前会话变为空闲”或“main 变为活跃”这两个时机切换,绝不会在活动进行中反复横跳。
这一策略在 WorkActivityStore.swift 中有完整的代码印证:setJobActive/setToolActive中,当新活动的role == .main时立即抢占currentSessionKey;否则仅在currentSessionKey == nil或当前会话已不活跃时才切换。pickNextSession()则优先检查 main 是否活跃,其次按lastUpdate时间戳选出最近更新的会话(WorkActivityStore.swift)。
活动类型(ActivityKind)与角色(SessionRole)
底层用两个枚举为状态建模(IconState.swift):
enum SessionRole { case main case other } enum ToolKind: String, Codable { case bash, read, write, edit, attach, other } enum ActivityKind: Codable, Equatable { case job case tool(ToolKind) }两类活动事件的含义:
job:高层命令执行(command execution),携带state: started | streaming | done | error,代表一次完整任务的开始与结束;tool:工具调用过程,携带phase: start | result,以及toolName(即name)和可选的meta/args信息。
在 WorkActivityStore.swift 的handleJob中,started与streaming被视作任务开始,其余状态(done/error/aborted等)则清空该会话的 job 与 tool 记录。
三、IconState 枚举:四种状态的完整建模
IconState是菜单栏图标状态的顶层抽象(IconState.swift),共四种取值:
| 状态 | 含义 |
|---|---|
idle | 空闲,无会话工作 |
workingMain(ActivityKind) | main 会话正在工作,携带具体活动类型 |
workingOther(ActivityKind) | 非 main 会话正在工作 |
overridden(ActivityKind) | 调试覆盖态,忽略真实活动、强制展示指定图标 |
每个状态还推导出两个派生属性:
badgeSymbolName:将活动类型映射为 SF Symbol 名称;badgeProminence:徽标显著度(primary/secondary/overridden),用于控制徽标在图标上的视觉强度。
enum IconState: Equatable { case idle case workingMain(ActivityKind) case workingOther(ActivityKind) case overridden(ActivityKind) enum BadgeProminence: Equatable { case primary case secondary case overridden } var badgeSymbolName: String { switch self.activity { case .tool(.bash): "chevron.left.slash.chevron.right" case .tool(.read): "doc" case .tool(.write): "pencil" case .tool(.edit): "pencil.tip" case .tool(.attach): "paperclip" case .tool(.other), .job: "gearshape.fill" } } }isWorking则区分忙碌与空闲——idle返回false,其余一律为true,供上层判断是否需要暂停健康状态展示。
四、ActivityKind → 图标字形:五类活动 + 默认
ActivityKind到图标字形(glyph)的映射遵循如下规则(文档原始定义,源码中以 SF Symbol 名称一一对应):
| ActivityKind | 字形 |
|---|---|
exec(命令执行) | 💻 |
read(读取) | 📄 |
write(写入) | ✍️ |
edit(编辑) | 📝 |
attach(附件) | 📎 |
| 默认(其他 / job) | 🛠️ |
在源码实现中,ToolKind的映射是:bash/shell→.bash,read→.read,write→.write,edit→.edit,attach→.attach,其余 →.other(见 WorkActivityStore.swift 的mapToolKind)。对应 SF Symbol 为chevron.left.slash.chevron.right(终端)、doc(文档)、pencil(写入)、pencil.tip(编辑)、paperclip(附件)、gearshape.fill(默认)。
五、视觉映射:idle / workingMain / workingOther / overridden 的呈现差异
四种状态在菜单栏图标的视觉呈现上有明确区分(源码见 CritterIconRenderer.swift 的makeIcon与drawBadge):
idle:显示普通 Critter,无徽标,正常眨眼的空闲神态;workingMain:显示徽标(badge)+ 完整色调(full tint)+ 腿部“工作”动画(leg working animation),即 Critter 的小腿摆动起来,视觉上“正在奔跑”;workingOther:显示徽标 + 弱化色调(muted tint),且没有腿部奔跑动画(no scurry),以区分忙碌来源的优先级;overridden:无视真实活动,强制使用用户选择的字形与色调。
徽标强度由BadgeProminence控制:primary为 1.0、secondary为 0.58、overridden为 0.85(CritterIconRenderer.swift)。绘制时先以 36×36px(18pt 逻辑画布)保证 Retina 清晰度,再在右上角绘制“puck”式徽标:扩大直径、以 clear 混合模式镂空出 SF Symbol 形状,最后以isTemplate = true输出,适配菜单栏模板渲染。
六、状态行文案(Status Row Text):工作态与空闲态
菜单第一行的状态文本遵循:
- 工作态:
<会话角色> · <活动标签>- 示例:
Main · exec: pnpm test - 示例:
Other · read: apps/macos/Sources/Clawdbot/AppState.swift
- 示例:
- 空闲态:回落到健康状态摘要(health summary)。
活动标签由WorkActivityStore.buildLabel生成(WorkActivityStore.swift),其规则:
exec:取args.command的首行;read/write:展示缩短后的文件路径;edit:展示路径 + 根据meta/ diff 计数推断的变更类型;- 兜底:直接展示工具名。
标签渲染进一步由ToolDisplayRegistry支撑(ToolDisplay.swift):它读取一份可解码的工具显示配置(含emoji、title、label、detailKeys与按action细分的actions),并根据工具的name、args(如action字段)与meta解析出label + detailLine的组合文案。detailLine由动词(verb)与细节(detail)以·连接而成,最终形成类似Main · bash: pnpm test的完整状态行。
七、事件摄取链路:从控制通道到状态机
7.1 事件源与解析
菜单栏状态的事件源是控制通道(control-channel)上的agent事件。链路如下(ControlChannel.swift):
startEventStream()通过GatewayConnection.shared.subscribe()订阅网关推送流;- 每条 push 在主线程进入
handle(push:),若evt.event == "agent"则用GatewayPayloadDecoding.decode解码为ControlAgentEvent; - 解码成功后先写入
AgentEventStore(供事件窗口回放),再调用routeWorkActivity(from:)路由到工作状态机; routeWorkActivity提取sessionKey(缺失时默认"main"),再按stream分发:stream == "job":读取data.state并调用WorkActivityStore.shared.handleJob(sessionKey:state:);stream == "tool":读取data.phase、data.name、data.meta,并将data.args通过bridgeToProtocolArgs桥接为协议参数后调用handleTool(...)。
解析字段汇总:
| 字段 | 来源 | 用途 |
|---|---|---|
stream: "job" | 事件流名 | 任务级状态,配data.state(started/streaming 开始,其余结束) |
stream: "tool" | 事件流名 | 工具级状态,配data.phase(start/result)、name、可选meta/args |
sessionKey | data.sessionKey | 会话归属,默认"main" |
7.2 状态机与防抖动
WorkActivityStore内部以两个字典维护活跃活动:
private var jobs: [String: Activity] = [:] private var tools: [String: Activity] = [:]jobs:按会话键保存进行中的 job;tools:按会话键保存进行中的 tool 调用;toolSeqBySession:记录每个会话的工具序号,用于结果事件去抖。
关键防抖动设计:工具result事件到来时,并不立即移除工具活动,而是延迟2.0 秒(toolResultGrace = 2.0)后、且确认期间没有新的start序列号产生,才清除该工具活动(WorkActivityStore.swift)。这正是“快速工具突发时徽标不闪烁”的底层保障。
refreshDerivedState()每次增删活动后都会重算currentSessionKey、current与iconState,其中deriveIconState()依据当前会话角色产出workingMain/workingOther/idle(WorkActivityStore.swift)。MenuBar的effectiveIconState再决定是直接采用状态机结果,还是应用调试覆盖。
八、调试覆盖(Debug Override):强制指定图标状态
当需要调试或演示特定工作态时,可通过Settings ▸ Debug ▸ Experiments ▸ “Icon override”选择器强制指定图标状态(DebugSettings.swift),选项包括:
System (auto)(默认):完全由状态机自动推导;Working: main:按工具类型强制展示 main 工作态(bash / read / write / edit / other 五种);Working: other:按工具类型强制展示非 main 工作态;Idle:强制空闲。
底层实现:
- 选择器以
@AppStorage(iconOverrideKey)持久化原始字符串(UserDefaults键为"clawdbot.iconOverride",见 Constants.swift); AppState.iconOverride类型为IconOverrideSelection,didSet时写入UserDefaults(AppState.swift);IconOverrideSelection是String类型的CaseIterable枚举(IconState.swift),共 11 个 case:system、idle、mainBash/mainRead/mainWrite/mainEdit/mainOther、otherBash/otherRead/otherWrite/otherEdit/otherOther,通过toIconState()映射为IconState;- 选择变更时,
DebugSettings的bindingOverride会同步调用AppStateStore.shared.iconOverride = selection与WorkActivityStore.shared.resolveIconState(override: selection)(DebugSettings.swift); MenuBar.effectiveIconState最终裁决:选择为.system时采用状态机的activityStore.iconState,否则将覆盖选择映射为.overridden(kind)或.idle(MenuBar.swift)。
注意区分两处覆盖语义:IconOverrideSelection.system会回落到真实状态,而其它选择会进入IconState.overridden,后者无视真实活动、以所选字形与色调渲染。
九、测试清单:验证状态机的五个关键场景
文档提供的测试清单可作为验证与回归的核心依据,结合源码可以确认每个场景对应的代码路径:
- 触发 main 会话 job:验证图标立即切换且状态行显示 main 标签。对应
handleJob中role == .main立即抢占currentSessionKey的路径。 - main 空闲时触发非 main job:图标/状态行展示非 main,且保持稳定直到其完成。对应
pickNextSession优先 main、其次按lastUpdate选取最近会话的逻辑。 - other 活跃时启动 main:图标瞬间翻转为 main。对应
setJobActive/setToolActive中 main 预占(preempts immediately)的分支。 - 快速工具突发:确保徽标不闪烁。对应
toolResultGrace = 2.0秒延迟清除 + 序列号守卫(guard self.toolSeqBySession[key, default: 0] == seq else { return })。 - 全部会话空闲后健康行回归:对应
refreshDerivedState在currentSessionKey失活时置空、iconState回落idle后健康摘要重新展示。
十、延伸:Nodes 区块与 Usage 区块的展示边界
菜单中的Nodes区块仅列出设备——即通过node.list配对的节点(Node),而非客户端或 presence 条目。相关渲染集中在 NodesMenu.swift,它基于NodeInfo结构体渲染节点名称、平台、版本与角色文本(如primaryName、roleText、versionLabels等辅助方法),并区分 gateway 节点与普通设备节点。
Usage区块则在提供商用量快照可用时出现于 Context 下方,用于展示模型提供商的使用量/成本信息。其数据来源于UsageData/UsageCostData模型与 UsageMenuLabelView.swift 的渲染组件,属于菜单栏的补充信息面,与工作状态展示互不干扰。
十一、相关实现文件索引
| 职责 | 文件 |
|---|---|
| 菜单栏入口、状态项装配、图标状态裁决 | MenuBar.swift |
| 状态枚举与覆盖选择 | IconState.swift |
| 工作状态机(会话裁决、防抖、标签构建) | WorkActivityStore.swift |
| 控制通道事件摄取与路由 | ControlChannel.swift |
| Critter 图标绘制与徽标渲染 | CritterIconRenderer.swift |
| 调试覆盖 UI 与持久化 | DebugSettings.swift |
| 工具标签显示解析 | ToolDisplay.swift |
| Nodes 区块渲染 | NodesMenu.swift |
| 用量区块渲染 | UsageMenuLabelView.swift |
本文档最初定位为菜单栏状态逻辑的开发参考(read_when: Tweaking mac menu UI or status logic),其原始内容见 docs/platforms/mac/menu-bar.md。如需深入理解底层事件契约,可继续阅读 GatewayModels.swift 中的AgentEvent定义(含runId、seq、stream、ts、data字段)。
- 人工智能
- AI Agent
- 即时通讯
- 后端
- 本地部署
- 语音
【免费下载链接】openclaw-cn
中文社区版OpenClaw,同原版保持定期更新,已内置钉钉、企业微信、飞书、QQ、微信以及国内网络环境优化。你的专属个人AI助手。支持所有操作系统和平台。🦞
相关推荐
openclaw-cn macOS 菜单栏图标动画状态机:Critter 图标的空闲、暂停、语音耳朵与工作忙碌动画实现指南
openclaw cn macOS 菜单栏图标动画状态机:Critter 图标的空闲、暂停、语音耳朵与工作忙碌动画实现指南 openclaw cn 的 macO
人工智能AI Agent即时通讯后端本地部署语音OpenClaw macOS 菜单栏状态逻辑全解析:图标、会话优先级与活动事件接入
OpenClaw macOS 菜单栏状态逻辑全解析:图标、会话优先级与活动事件接入 导读 本文基于 docs/platforms/mac/menu bar.md
人工智能大模型LLM 网关AI Agent即时通讯AntdUI图标状态:IconState的图标显示与状态管理
AntdUI图标状态:IconState的图标显示与状态管理 在现代桌面应用开发中,状态反馈是提升用户体验的关键因素。AntdUI作为基于Ant Design设
UI组件桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考