Menu Bar Icon States and Animations for OpenClaw on macOS

This page documents the four states of the OpenClaw menu bar icon on macOS: Idle, Paused, Sleeping, and Celebrate. It is intended for developers working on the macOS app.

Read this when

  • Changing menu bar icon behavior

Scope: macOS app (apps/macos). Rendering: CritterIconRenderer.makeIcon(...). Animation/state wiring: CritterStatusLabel + CritterStatusLabel+Behavior.swift.

States

StateTriggerVisual
IdleDefaultNormal blink/wiggle animation; open eyes keep a glossy glint
PausedisPaused=trueAntennae droop ("off duty") with open eyes; no motion
SleepingGateway disconnected/unconfiguredAntennae droop and eyes close into ⌣ ⌣ lids; no motion
CelebrateMessage sent (sendCelebrationTick)Eyes flash happy ∩ ∩ arcs for ~0.9s plus a leg kick
Voice wake (big ears)Wake word heardAntennae perk up straight and taller (earScale=1.9); drops after silence
WorkingisWorking=true or an active IconStateFaster leg wiggle (legWiggle up to 1.0) plus a small horizontal offset; additive to idle wiggle

A tool-activity badge (SF Symbol puck, e.g. chevron.left.slash.chevron.right for exec) can render on top of the same critter icon when a session has an active job or tool. That badge comes from IconState/ActivityKind; see Menu bar for the full state model.

Voice wake ears

  • Trigger: AppStateStore.shared.triggerVoiceEars(ttl: nil), called from the voice-wake capture pipeline (VoiceWakeRuntime) and from voice-wake debug/test tooling (VoiceWakeTester, VoiceWakeOverlayController).
  • Stop: stopVoiceEars(), called when capture finalizes.
  • Silence window before finalizing: 2.0s normally, 5.0s if only the trigger word was heard and no further speech followed (VoiceWakeRuntime.silenceWindow / triggerOnlySilenceWindow).
  • While boosted, idle blink/wiggle/leg/ear timers are suspended (earBoostActive gates the animation task in CritterStatusLabel+Behavior).

Shapes and sizes

  • Canvas: 18x18pt template image, rendered into a 36x36px bitmap backing store (2x) so the icon stays crisp on Retina.
  • Ear scale defaults to 1.0; voice boost sets earScale=1.9 without changing the overall frame.
  • antennaDroop (0-1) folds the antennae down for the paused and sleeping poses.
  • Leg scurry uses legWiggle up to 1.0 with a small horizontal jiggle.

Behavioral notes

  • No external CLI/broker toggle for ears or working state; both are driven internally by app signals (AppState.setWorking, AppState.triggerVoiceEars) to avoid accidental flapping.
  • Keep any new TTL short (well under 10s) so the icon returns to baseline quickly if a job hangs.