Skip to content

Menu bar status item for busy AI agents - #670

Open
ivnnv wants to merge 7 commits into
gnachman:masterfrom
ivnnv:ai-menu-bar-status
Open

Menu bar status item for busy AI agents#670
ivnnv wants to merge 7 commits into
gnachman:masterfrom
ivnnv:ai-menu-bar-status

Conversation

@ivnnv

@ivnnv ivnnv commented May 23, 2026

Copy link
Copy Markdown
Contributor

Opt-in menu bar status item showing how many AI agents are currently working. Off by default, gated on the new ShowMenuBarItem advanced setting. The existing UIElement plus statusBarIcon combination still installs the same item for dock-less mode.

The icon recolors the existing StatusItem template asset orange and replaces the dollar glyph with the count (1 to 9, or + at 10 or more). Idle keeps isTemplate true so light/dark menu bars auto-tint.

Count is the union of two sources, deduped by namespaced id: terminal sessions whose OSC 21337 status text starts with "Working" (case-insensitive — so Working, Working…, Working… 24s all count, but Waiting/Idle/custom statuses don't), and ChatBroker chats whose agent participant is currently typing (the built-in AI chat).

Screenshot 2026-05-23 at 23 59 02 Screenshot 2026-05-23 at 23 58 57 Screenshot 2026-05-23 at 23 58 51 Screenshot 2026-05-23 at 23 58 40

Test plan:

  • Toggle Settings > Advanced > ShowMenuBarItem on; idle dollar icon appears.
  • In any pane, run printf '\e]21337;status=Working;indicator=#ffa500\a' and confirm orange icon with count 1; repeat in more panes to climb, + at 10 or more.
  • Clear with printf '\e]21337;status=;indicator=\a' and confirm idle returns.
  • Set a non-working status with printf '\e]21337;status=Waiting;indicator=#5f87ff\a' and confirm the badge does not tick up. Same with status=Idle.
  • Open the built-in AI chat, send a prompt, confirm the badge ticks up while the agent is typing.

@gnachman

Copy link
Copy Markdown
Owner

Interesting idea but I think the scope is too narrow. I can see the value of a global status indicator but it needs to be useful more broadly to justify the added complexity. When clicked it should show something useful, like a list of sessions sorted by status, maybe integrate with triggers, which sessions have had bells. I'll keep mulling this over.

@ivnnv

ivnnv commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

sure @gnachman, I wanted to keep this one atomic but yeah this could be expanded on click to show the status of each of the tabs/agents right there and clicking to take you directly to the upfront app, at the given tab/agent you select, I could implement it in this very PR if you want 👍🏽

@ivnnv
ivnnv marked this pull request as draft May 31, 2026 08:36
carvalhoviniciusluiz added a commit to carvalhoviniciusluiz/it2agent that referenced this pull request Jul 22, 2026
Import upstream gnachman#670 ("Menu bar status item for busy
AI agents") behind the new spawnterm.agent_menubar capability flag
(default OFF). When the flag is OFF, behavior is identical to stock
iTerm2: the controller only shows a status item for the legacy
"excluded from Dock + status bar icon" combination and never draws a
busy-count badge. When the flag is ON, the independent menu bar item
(gated additionally by the ShowMenuBarItem Advanced setting) and the
orange busy-agent count badge become available.

The flag is the master switch, wired in the three synced places
(spawnterm-flag, spawnterm_flag.py, iTermSpawnTermCapabilities) plus
the docs, and gated inside AIMenuBarStatusController.refreshOnMain.

Adapted from upstream gnachman#670.

Co-authored-by: carvalhoviniciusluiz <carvalhoviniciusluiz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
carvalhoviniciusluiz added a commit to carvalhoviniciusluiz/it2agent that referenced this pull request Jul 22, 2026
…an#670/gnachman#673) + 3 new AI flags + agent guide

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gnachman gnachman left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Nice feature. A few things I'd like addressed before merging.

1. Busy-session count should key off the "Working" status text, not hasIndicator.

In busyCount() you filter with .filter { $0.hasIndicator }, but the PR description and test plan say only sessions whose status text starts with "Working" should count. hasIndicator is set purely by the OSC 21337 indicator= field, independent of the status text, so:

  • The test-plan case printf '\e]21337;status=Waiting;indicator=#5f87ff\a' sets hasIndicator = true and will tick the badge up, failing the step that expects it not to.
  • More importantly, WorkgroupIntrospection.swift already documents this exact pitfall: "cc-status always sets a dot color even at idle, so hasIndicator alone is misleading... would make every Claude Code session look perpetually working." So every idle Claude Code session inflates the count.

The correct classification already exists. Please reuse WorkgroupIntrospection.state(forTabStatus:) (which switches on statusText.lowercased()), or at minimum filter on statusText?.lowercased().hasPrefix("working") to match the stated semantics.

2. Remove the now-dead statusBarItem property on iTermApplication.

This PR deletes the only writes to self.statusBarItem (the old install/remove block in setIsUIElement:), and there are no other readers anywhere. Please drop the @property declaration in both iTermApplication.h and iTermApplication.m so nobody assumes it still tracks a live status item.

3. Avoid refreshing on every defaults write.

defaultsChanged is wired to UserDefaults.didChangeNotification, which fires on any defaults write (we write defaults frequently). When the item is visible, each one runs updateImage() and re-renders a fresh NSImage even when the count is unchanged. Please observe only the relevant key(s), or short-circuit when the computed count and pref state haven't changed.

(1) is the important one; (2) and (3) are cleanup.

ivnnv added 5 commits August 3, 2026 08:49
A new "Show menu bar item" preference (Settings > Advanced,
ShowMenuBarItem) installs an NSStatusItem independent of the existing
"Exclude from Dock" gating. The icon shows the template StatusItem
image plus an orange badge with the count of currently-working agents
(pulled from TypingStatusModel), and the badge disappears when no
agents are working. Clicking opens the existing status bar menu.

Adds TypingStatusModel.chatIDs(forParticipant:) so the controller can
recompute the busy set after each typingStatus broker update without
extending the Update enum. The legacy inline NSStatusItem creation in
iTermApplication setIsUIElement: is replaced by a refresh call into
the new controller, which still honors the existing UIElement +
statusBarIcon advanced setting combination for backward compatibility.
ChatBroker delivers updates synchronously on the publisher's thread, so
the controller's typing-status handler was mutating state and touching
AppKit off-main. Hop to the main queue before reading busyChatIDs or
updating the status item.

The previous icon implementation drew the template glyph into a bitmap
and assigned the composite with isTemplate=false, which broke automatic
tinting in dark menu bars. Replace it with a layered approach: the
status item button keeps the unmodified template image, and a custom
BadgeView subview draws the orange count badge in the top-right corner
only when busy count > 0.

Promote the menu bar item toggle from a plain iTermPreferences key to
a real DEFINE_BOOL in iTermAdvancedSettingsModel so it appears in the
Settings > Advanced search UI.
Replace the badge-overlay approach with an icon rendered from the original StatusItem.png mask: tint the mask orange and replace the dollar glyph with the busy count (1-9, or + at 10 or more). When idle, the unmodified template image renders as before.

Count is the union of OSC 21337 tab indicators and ChatBroker agent typing chats, deduped by namespaced id so a Claude Code session in a terminal pane and a built-in AI chat both contribute one each.
Count a session as busy only when its OSC 21337 status text begins with "Working". hasIndicator is set independently of the status text, so Waiting and Idle sessions were counted, and agents that keep an indicator dot while idle (Claude Code) inflated the count permanently. Prefix rather than equality because emitters append progress to the label ("Working... 24s").

Drop iTermApplication.statusBarItem. Its only writes went away with the old install/remove block in setIsUIElement:, and nothing reads it.

Track the count last drawn into the button and skip updateImage when it has not changed. refresh runs on UserDefaults.didChangeNotification, which fires on every defaults write, and each one was re-rendering a fresh NSImage.
ChatBroker.Update gained a turnLifecycle case, so the subscription switch is no longer exhaustive. ChatBroker and TypingStatusModel are now @mainactor; both call sites already run on main, so assert that rather than hopping.
@ivnnv
ivnnv force-pushed the ai-menu-bar-status branch from 78a5744 to 16ae89b Compare August 3, 2026 05:57
ivnnv added 2 commits August 3, 2026 09:12
…elevant keys

Reuse the existing WorkgroupIntrospection.state(forTabStatus:) classifier instead of a second notion of busy living in the menu bar controller.

Replace the blanket UserDefaults.didChangeNotification observer with iTermUserDefaultsObserver on the three keys that feed shouldShow: HideFromDockAndAppSwitcher, ShowMenuBarItem, StatusBarIcon.
refreshOnMain read the raw HideFromDockAndAppSwitcher preference. With UIElementRequiresHotkeys also set, updateProcessType calls setIsUIElement:NO while ordinary windows are open, and the old inline code in setIsUIElement: removed the status item at that point. Reading iTermApplication.isUIElement restores that behavior.

Also make init private so the type is only reachable through sharedInstance.
@ivnnv

ivnnv commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

hey @gnachman

All three done, plus rebased on master (was 610 behind).

Found one more while in there: refreshOnMain read the raw HideFromDockAndAppSwitcher pref instead of the effective isUIElement, so with UIElementRequiresHotkeys on the legacy item stayed after updateProcessType turned it off.

You were right about point 1 and I could confirm it live: cc-status sets lowercase working / idle but keeps hasIndicator true in BOTH, so the old filter counted idle sessions forever. Badge does 0 -> 1 -> 0 now.

@ivnnv
ivnnv marked this pull request as ready for review August 3, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants