What is shown
- The current agent work state renders in the menu bar icon and in the first status row of the menu.
- Health status is hidden while work is active; it returns once all sessions are idle.
- A root “Context” item opens a submenu with recent sessions instead of expanding them in the root menu.
- A “Nodes” block in the root menu lists paired devices only (from
node.list), not client/presence entries. - A root “Usage” section appears below Context when provider usage snapshots are available, followed by cost details when available.
State model
- Source:
WorkActivityStore(apps/macos/Sources/OpenClaw/WorkActivityStore.swift). - Events arrive as
ControlAgentEventwith arunId; the handler (ControlChannel.routeWorkActivity) readssessionKeyfrom the event payload and defaults to"main"if absent. - Priority: the main session (
sessionKey == "main"by default) always wins. If main is active, its state shows immediately. If main is idle, the most recently active non-main session shows instead. The store does not flip mid-activity; it only switches when the current session goes idle or main becomes active. - Activity kinds:
job: high-level command execution (state: started|streaming|done|error|...).tool:phase: start|resultwithname, optionalmeta/args.
IconState enum (Swift)
idleworkingMain(ActivityKind)workingOther(ActivityKind)overridden(ActivityKind)(debug override)
ActivityKind -> badge symbol
ActivityKind wraps a ToolKind (bash, read, write, edit, attach, other) or a bare job. Each maps to an SF Symbol badge drawn over the critter icon (IconState.badgeSymbolName):
| Kind | Symbol |
|---|---|
bash | chevron.left.slash.chevron.right |
read | doc |
write | pencil |
edit | pencil.tip |
attach | paperclip |
other / job | gearshape.fill |
Visual mapping
idle: normal critter, no badge.workingMain: badge with symbol, full tint (.primaryprominence), leg “working” animation.workingOther: badge with symbol, muted tint (.secondaryprominence), no scurry.overridden: uses the chosen symbol/tint regardless of real activity.
Context submenu
- The root menu shows one “Context” row with a session count/status; it opens a submenu (
MenuSessionsInjector). - The submenu header shows the active session count for the last 24 hours.
- Each session row keeps its token bar, age, preview, thinking/verbose toggle, reset, compact, and delete actions.
- Loading, disconnected, and session-load error messages render inside the Context submenu.
- Usage and cost sections stay root-level below Context so they remain glanceable without opening the submenu.
Status row text (menu)
- While work is active:
<Session role> · <activity label>("\(roleLabel) · \(activity.label)"inMenuContentView), where role label isMainorOther. - When idle: falls back to the health summary.
Event ingestion
- Source: control-channel
agentevents, routed byControlChannel.routeWorkActivity(from:). - Parsed fields:
stream: "job"withdata.statefor start/stop.stream: "tool"withdata.phase,data.name, optionaldata.meta/data.args.
- Tool labels come from
ToolDisplayRegistry.resolve(name:args:meta:); unresolved names fall back to the raw tool name.
Debug override
- Settings > Debug > “Icon override” picker:
System (auto)(default)Working: main/Working: other(per tool kind: bash, read, write, edit, other)Idle
- Stored under
UserDefaultskeyopenclaw.iconOverride; mapped toIconState.overridden.
Testing checklist
- Trigger main session job: icon switches immediately and status row shows the main label.
- Trigger non-main session job while main is idle: icon/status shows the non-main session; stays stable until it finishes.
- Start main while another session is active: icon flips to main instantly.
- Rapid tool bursts: badge does not flicker (2s grace window before clearing a finished tool,
WorkActivityStore.toolResultGrace). - Health row reappears once all sessions are idle.