Menu Bar State Logic and Display for macOS Agent

This page explains how the menu bar icon and status row reflect the current agent work state, including session priority and hidden health status. It is intended for developers integrating or debugging the menu bar behavior on macOS.

Read this when

  • Tweaking mac menu UI or status logic

What is shown

  • The menu bar icon and the first status row of the menu display the current agent work state.
  • Health status is hidden while work is active and reappears once all sessions are idle.
  • A root "Context" item opens a submenu containing recent sessions rather than expanding them directly in the root menu.
  • A "Nodes" block in the root menu only lists paired devices (from node.list), not client or presence entries.
  • A root "Usage" section appears below Context when provider usage snapshots are available, followed by cost details if present.
  • Quick Chat opens the floating main-session composer; its current global shortcut is shown next to the item.

State model

  • Source: WorkActivityStore (apps/macos/Sources/OpenClaw/WorkActivityStore.swift).
  • Events come as ControlAgentEvent with a runId; the handler (ControlChannel.routeWorkActivity) reads sessionKey from the event payload and defaults to "main" if it is missing.
  • Priority: the main session (sessionKey == "main" by default) always takes precedence. If main is active, its state appears immediately. If main is idle, the most recently active non-main session is shown instead. The store does not switch mid-activity; it only changes 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|result with name, optional meta/args.

IconState enum (Swift)

  • idle
  • workingMain(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):

KindSymbol
bashchevron.left.slash.chevron.right
readdoc
writepencil
editpencil.tip
attachpaperclip
other / jobgearshape.fill

Visual mapping

  • idle: normal critter, no badge.
  • workingMain: badge with symbol, full tint (.primary prominence), leg "working" animation.
  • workingOther: badge with symbol, muted tint (.secondary prominence), no scurry.
  • overridden: uses the chosen symbol and tint regardless of real activity.

Context submenu

  • The root menu shows one "Context" row with a session count and status; it opens a submenu (MenuSessionsInjector).
  • The submenu header displays the active session count for the last 24 hours.
  • Each session row retains its token bar, age, preview, thinking/verbose toggle, reset, compact, and delete actions.
  • Loading, disconnected, and session-load error messages appear 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)" in MenuContentView), where the role label is Main or Other.
  • When idle: falls back to the health summary.

Event ingestion

  • Source: control-channel agent events, routed by ControlChannel.routeWorkActivity(from:).
  • Parsed fields:
    • stream: "job" with data.state for start/stop.
    • stream: "tool" with data.phase, data.name, optional data.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 UserDefaults key openclaw.iconOverride; mapped to IconState.overridden.

Testing checklist

  • Trigger main session job: icon switches immediately and the status row shows the main label.
  • Trigger non-main session job while main is idle: icon and status show the non-main session; it stays stable until that session 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.