Menu bar status item for busy AI agents - #670
Conversation
|
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. |
|
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 👍🏽 |
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>
…an#670/gnachman#673) + 3 new AI flags + agent guide Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gnachman
left a comment
There was a problem hiding this comment.
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'setshasIndicator = trueand will tick the badge up, failing the step that expects it not to. - More importantly,
WorkgroupIntrospection.swiftalready 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.
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.
78a5744 to
16ae89b
Compare
…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.
|
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. |
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).
Test plan:
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.printf '\e]21337;status=;indicator=\a'and confirm idle returns.printf '\e]21337;status=Waiting;indicator=#5f87ff\a'and confirm the badge does not tick up. Same withstatus=Idle.