Skip to content

Fix stale terminal agent tab icons - #7740

Merged
austinywang merged 8 commits into
mainfrom
issue-7731-agent-tab-icon-stale
Jul 10, 2026
Merged

austinywang merged 8 commits into
mainfrom
issue-7731-agent-tab-icon-stale

Conversation

@austinywang

@austinywang austinywang commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7731

Root cause

Terminal tab icon state had multiple inputs: PID-derived live agent runtime, title-derived agent fallback, and restored-session metadata. On terminal title changes, cmux updated the title-derived agent key and the tab title, but did not prune dead PID-derived runtime for that same panel before resolving the icon. Because TerminalTabAgentIconResolver correctly gives live runtime precedence over title fallback, a dead Claude claude_code PID key could keep winning after the user exited Claude and launched Codex in the same PTY.

This was in the cmux app state path, not Bonsplit: Bonsplit already supports clearing or replacing iconAsset when cmux sends the update.

Fix

On terminal title updates, Workspace.updatePanelTitle now reconciles title-derived agent state and prunes stale per-panel agent PID runtime before resolving the tab icon. That makes the icon reflect the current live-or-title-derived agent signal and covers Claude -> Codex, Codex -> Claude, and agent -> plain shell transitions.

Tests

Added behavioral Swift Testing coverage through Workspace.updatePanelTitle for:

  • stale Claude runtime + Codex title => Codex icon
  • stale Codex runtime + Claude title => Claude icon
  • stale agent runtime + plain shell title => icon clears

The first commit adds only the regression tests; the second commit adds the fix.

Local verification

  • git diff --check
  • ./scripts/lint-pbxproj-test-wiring.sh
  • python3 scripts/swift_file_length_budget.py currently reports pre-existing over-budget files not touched by this PR: Sources/DockSplitStore.swift, Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift, and Sources/Panels/BrowserNavigationDelegate.swift.

No local app build or local Xcode tests were run before opening the PR because the issue instructions required GitHub Actions for validation.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches workspace agent lifecycle, tab icon sync, and notification cleanup on a hot path (title updates), but changes are localized with strong regression tests.

Overview
Fixes stale terminal agent tab icons when users switch agents in the same PTY (e.g. Claude → Codex) by reconciling icon state on panel title changes.

Workspace.updatePanelTitle now calls clearStaleAgentPIDs(panelId:) for terminal panels before resolving the tab icon. Dead PID-derived runtime no longer wins over title-derived agent fallback in TerminalTabAgentIconResolver. Bonsplit tab icon updates when stale runtime is pruned even if the title text is unchanged.

clearStaleAgentPIDs (workspace-wide and per-panel) now clears stale terminal notifications when pruning succeeds—per-panel cleanup is scoped with surfaceId. TabManager.sweepStaleAgentPIDs no longer clears notifications itself (that happens inside workspace pruning).

Regression tests cover title-only agent identity, Claude↔Codex transitions, plain shell icon clear, port cleanup, and panel-scoped notification removal.

Reviewed by Cursor Bugbot for commit 74f1c5e. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes stale terminal agent tab icons by pruning dead PID runtime during title updates and clearing stale notifications so the icon matches the current agent or a plain shell. Centralizes cleanup and port refresh in Workspace, and updates the bonsplit icon when the agent signal changes even if the title text doesn’t.

  • Bug Fixes
    • In Workspace.updatePanelTitle, reconcile title-derived agent, prune per-panel agent PIDs via clearStaleAgentPIDs(refreshPorts: true), and clear stale notifications (tab-wide or per-panel) before resolving the icon; refresh the bonsplit icon on title, agent-fallback, or pruned-runtime changes.
    • Moved notification clearing out of TabManager.sweepStaleAgentPIDs; cleanup now happens in Workspace, preserving sibling panel notifications. Added tests for Claude↔Codex, agent→shell (icon clears), stale port cleanup, panel-scoped notification cleanup, and that raw titles like "codex --yolo" don’t become agent identity.

Written for commit 74f1c5e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved Terminal tab icon refresh when panel titles change, including when stale agent runtime PIDs are pruned.
    • Prevented outdated Claude/Codex icons from persisting after title updates.
    • Ensured non-agent shell titles (e.g., zsh) correctly clear any lingering agent icon state.
    • Improved consistency of stale agent related UI notification cleanup.
  • Tests
    • Added regression coverage for Claude↔Codex icon transitions and clearing icons for shell-title cases.

@vercel

vercel Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 10, 2026 12:12am
cmux-staging Building Building Preview, Comment Jul 10, 2026 12:12am

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Terminal title updates now prune stale agent runtime PIDs, clear related notifications, and refresh tab icons when runtime or title-derived agent state changes. Tests cover Claude, Codex, and plain-shell transitions.

Changes

Stale agent cleanup and icon refresh

Layer / File(s) Summary
Centralize stale runtime and notification cleanup
Sources/Workspace+PanelLifecycle.swift, Sources/TabManager.swift
Stale PID cleanup optionally refreshes tracked ports and clears tab or surface notifications; periodic sweeping delegates cleanup without separate notification handling.
Refresh icons after panel title updates
Sources/TerminalTabAgentIcon.swift
Terminal title updates track stale-runtime pruning and refresh Bonsplit tab and terminal icon state when pruning or title-derived agent resolution changes.
Validate stale agent icon transitions
cmuxTests/TerminalTabAgentIconTests.swift
Tests cover Claude-to-Codex, Codex-to-Claude, and agent-to-shell transitions, including stale PID, listening-port, notification, and icon state cleanup.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Terminal
  participant Workspace
  participant NotificationStore
  participant BonsplitTab
  Terminal->>Workspace: updatePanelTitle(panelId, title)
  Workspace->>Workspace: clearStaleAgentPIDs(panelId, refreshPorts: true)
  Workspace->>NotificationStore: clearNotifications(forTabId, surfaceId)
  Workspace->>BonsplitTab: refresh tab and icon after stale-runtime pruning
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change addresses #7731 by re-evaluating icons after stale PID cleanup and covers Claude↔Codex and shell transitions in tests.
Out of Scope Changes check ✅ Passed The changes stay focused on stale agent icon cleanup, PID pruning, notification cleanup, and regression tests with no unrelated scope.
Cmux Swift Actor Isolation ✅ Passed Workspace and TerminalNotificationStore are @MainActor, and the new tests use @MainActor; the diff adds no new cross-actor mutable state.
Cmux Swift Blocking Runtime ✅ Passed Diff adds stale PID pruning/notification cleanup only; no new waits, sleeps, syncs, polling, or locks in production code, and tests are non-blocking.
Cmux Browser Automation Off-Main ✅ Passed No browser automation commands, worker routing, or policy/test files were changed; the diff only touches stale agent PID cleanup and notifications.
Cmux Expensive Synchronous Load ✅ Passed PR only adds in-memory stale PID pruning/notification cleanup on title updates; no RestorableAgentSessionIndex.load() or large file/JSON parsing was added or moved.
Cmux Cache Substitution Correctness ✅ Passed The PR only refreshes transient tab icons after freshness-checked PID pruning; it doesn’t replace authoritative reads in any persistence/history/snapshot path.
Cmux No Hacky Sleeps ✅ Passed The PR only changes Swift files, and the no-hacky-sleeps rule applies to non-Swift runtime scripts; no new fixed waits were introduced.
Cmux Algorithmic Complexity ✅ Passed PASS — the new title-update path only adds a per-panel Set walk and dict/set lookups; no nested full-collection rescans or repeated sorting/filtering were introduced.
Cmux Swift Concurrency ✅ Passed The diff only adjusts synchronous stale-PID cleanup and notification clearing; no new DispatchQueue, Task, Combine, or completion-handler async patterns were added.
Cmux Swift @Concurrent ✅ Passed Touched methods are @MainActor/synchronous; no new nonisolated async work or @concurrent misuse appears in the diff.
Cmux Swift File And Package Boundaries ✅ Passed Focused fix only adds small state cleanup/tests; touched production files stay within thresholds and TabManager’s 1-line change is incidental.
Cmux Swiftpm Lockfiles ✅ Passed Only Sources/TabManager.swift and Sources/Workspace+PanelLifecycle.swift changed; no Package.swift, .gitignore, workflow, Xcode project, or Package.resolved files were touched.
Cmux Swift Logging ✅ Passed No diff-added print/debugPrint/dump/NSLog or problematic Logger changes; the patch only alters stale PID cleanup and notification clearing paths.
Cmux User-Facing Error Privacy ✅ Passed PR only adds agent-state pruning/cleanup logic; production diffs add no user-facing errors, alerts, or recovery copy.
Cmux Full Internationalization ✅ Passed No user-facing copy was added or changed; the PR only adjusts terminal state logic and tests, with no xcstrings/web message edits.
Cmux Swiftui State Layout ✅ Passed The PR only adjusts workspace/tab model reconciliation and tests; it adds no new SwiftUI view state/layout patterns, GeometryReader usage, or render-time mutation.
Cmux Architecture Rethink ✅ Passed PASS: The fix stays within Workspace’s existing state owner, pruning stale agent runtime during title reconciliation and reusing the same cleanup path without new timing/observer side channels.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Diff only adjusts tab/icon and stale-agent cleanup; no user-visible window/controller code or cmuxAuxiliaryWindowIdentifiers changes, so the rule doesn't apply.
Cmux Source Artifacts ✅ Passed Only hand-written Swift source files changed; no logs, caches, temp dirs, build output, or other artifacts appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Production edits only changed stale-agent cleanup in Workspace+PanelLifecycle.swift and TabManager.swift; no new debug/test-only members or guards were added.
Cmux No Ambient Global State ✅ Passed PASS: the patch only changes methods on Workspace/TabManager; no new file-scope funcs, globals, static-namespace types, or singletons were added. AppDelegate.shared use was pre-existing.
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing stale terminal agent tab icons.
Description check ✅ Passed The description covers the summary, root cause, fix, tests, and local verification, but it omits the demo video, review trigger, and checklist sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7731-agent-tab-icon-stale

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes stale terminal agent tab icons after agent changes in the same terminal. The main changes are:

  • Prunes stale per-panel agent runtime during terminal title updates.
  • Refreshes tracked agent ports when stale runtime is removed.
  • Updates the Bonsplit tab icon when the resolved agent signal changes.
  • Moves stale terminal notification cleanup into the PID pruning paths.
  • Adds tests for Claude, Codex, shell-title, port, and notification cleanup cases.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
Sources/TerminalTabAgentIcon.swift Updates terminal title handling so stale agent runtime can be pruned before resolving the tab icon.
Sources/Workspace+PanelLifecycle.swift Refreshes tracked ports and clears relevant notifications after stale agent PID cleanup.
Sources/TabManager.swift Removes duplicate notification cleanup from the periodic stale PID sweep.
cmuxTests/TerminalTabAgentIconTests.swift Adds tests for stale icon transitions, port cleanup, and scoped notification cleanup.

Reviews (5): Last reviewed commit: "Add no-title agent identity regression c..." | Re-trigger Greptile

Comment thread Sources/TerminalTabAgentIcon.swift Outdated
Comment thread Sources/TerminalTabAgentIcon.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/TerminalTabAgentIcon.swift`:
- Around line 198-207: Add a regression test alongside the existing terminal
title/icon tests covering the didPruneStaleAgentRuntime-only path, using the
relevant Workspace updatePanelTitle and agent PID recording APIs. Start with a
Codex-derived title, record a stale Claude PID, then change to another Codex
title that preserves the derived agent key; assert the stale PID is removed and
both the workspace icon and Bonsplit tab icon update to Codex.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0379ff03-7297-4e1f-bd87-b4d544c34d86

📥 Commits

Reviewing files that changed from the base of the PR and between e20d00c and a0a7257.

📒 Files selected for processing (2)
  • Sources/TerminalTabAgentIcon.swift
  • cmuxTests/TerminalTabAgentIconTests.swift

Comment thread Sources/TerminalTabAgentIcon.swift Outdated
Comment on lines +198 to +207
// Update bonsplit tab title only when this panel's title changed, and
// update the icon when the title-derived agent fallback changes.
if (didMutatePanelTitle || didMutateTitleDerivedAgent),
// update the icon when the current agent signal changes.
if (didMutatePanelTitle || didMutateTitleDerivedAgent || didPruneStaleAgentRuntime),
let tabId = surfaceIdFromPanelId(panelId),
let panel = panels[panelId],
let existing = bonsplitController.tab(tabId) {
let baseTitle = panelTitles[panelId] ?? panel.displayTitle
let resolvedTitle = resolvedPanelTitle(panelId: panelId, fallback: baseTitle)
let titleUpdate: String? = existing.title == resolvedTitle ? nil : resolvedTitle
let iconAssetUpdate: String?? = didMutateTitleDerivedAgent
let iconAssetUpdate: String?? = didMutateTitleDerivedAgent || didPruneStaleAgentRuntime

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the didPruneStaleAgentRuntime-only path.

All three test cases transition the title to a different agent (or plain shell), so didMutateTitleDerivedAgent is always true alongside didPruneStaleAgentRuntime. The specific path this fix enables — where didPruneStaleAgentRuntime is true but didMutateTitleDerivedAgent is false (e.g., title changes from "codex --yolo" to "codex --yolo --verbose", both mapping to "codex", while a stale Claude PID is pruned) — is not exercised. Without coverage, a future regression could drop didPruneStaleAgentRuntime from the icon-update condition and the existing tests would still pass.

🧪 Suggested additional test
`@MainActor`
`@Test` func terminalTitleChangePrunesStaleAgentWithoutTitleDerivedKeyChange() throws {
    let workspace = Workspace()
    let panel = try `#require`(workspace.focusedTerminalPanel)
    let tabId = try `#require`(workspace.surfaceIdFromPanelId(panel.id))

    // Establish title-derived key as "codex" first
    _ = workspace.updatePanelTitle(panelId: panel.id, title: "codex --yolo")

    // Record a stale Claude PID
    workspace.recordAgentPID(key: "claude_code.old-session", pid: 0, panelId: panel.id, refreshPorts: false)

    // Stale Claude icon should be showing (live agent wins over title-derived)
    `#expect`(workspace.terminalTabAgentIconAsset(forPanelId: panel.id) == "AgentIcons/Claude")

    // Title changes but still maps to "codex" — title-derived key doesn't change
    `#expect`(workspace.updatePanelTitle(panelId: panel.id, title: "codex --yolo --verbose"))
    // Stale Claude pruned, icon should now show Codex from title-derived key
    `#expect`(workspace.terminalTabAgentIconAsset(forPanelId: panel.id) == "AgentIcons/Codex")
    `#expect`(workspace.bonsplitController.tab(tabId)?.iconAsset == "AgentIcons/Codex")
    `#expect`(workspace.agentPIDs["claude_code.old-session"] == nil)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TerminalTabAgentIcon.swift` around lines 198 - 207, Add a regression
test alongside the existing terminal title/icon tests covering the
didPruneStaleAgentRuntime-only path, using the relevant Workspace
updatePanelTitle and agent PID recording APIs. Start with a Codex-derived title,
record a stale Claude PID, then change to another Codex title that preserves the
derived agent key; assert the stale PID is removed and both the workspace icon
and Bonsplit tab icon update to Codex.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Workspace+PanelLifecycle.swift (1)

148-160: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope stale PID notification clears to the owning panel(s). clearNotifications(forTabId:) removes every notification in the tab, but clearStaleAgentPIDs() can be triggered by just one dead PID among multiple panels. Use agentPIDPanelIdsByKey to call clearNotifications(forTabId:surfaceId:) for the affected panel(s), and only fall back to the tab-wide clear for unowned keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace`+PanelLifecycle.swift around lines 148 - 160, Update
clearStaleAgentPIDs so notification cleanup is scoped to affected panels: track
each stale PID’s key, use agentPIDPanelIdsByKey to call
clearNotifications(forTabId:surfaceId:) for every owning panel, and only call
the existing tab-wide clearNotifications(forTabId:) fallback when a stale key
has no owning panel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Workspace`+PanelLifecycle.swift:
- Around line 148-160: Update clearStaleAgentPIDs so notification cleanup is
scoped to affected panels: track each stale PID’s key, use agentPIDPanelIdsByKey
to call clearNotifications(forTabId:surfaceId:) for every owning panel, and only
call the existing tab-wide clearNotifications(forTabId:) fallback when a stale
key has no owning panel.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76c2031f-c81d-4976-a13e-1894e7b66578

📥 Commits

Reviewing files that changed from the base of the PR and between 5fc34c8 and 28a9cea.

📒 Files selected for processing (3)
  • Sources/TabManager.swift
  • Sources/Workspace+PanelLifecycle.swift
  • cmuxTests/TerminalTabAgentIconTests.swift

…icon-stale

# Conflicts:
#	Sources/TerminalTabAgentIcon.swift
#	cmuxTests/TerminalTabAgentIconTests.swift
@austinywang
austinywang merged commit bb029d8 into main Jul 10, 2026
29 checks passed
lawrencecchen added a commit that referenced this pull request Jul 10, 2026
Main reverted #7216 (workspaces-as-todos) and its file splits, so this merge
deletes WorkspaceTodoState/WorkspaceTodoPanel/WorkspaceTodoPanelView,
AllShortcutsPopover.swift, and VerticalTabsSidebar+EmptyAreasAndFooter.swift,
strips the todo fields from the sidebar-immediate and mobile observed
snapshots, and folds the returning ContentView sidebar footer/empty-area
types back in with the migration transforms applied (@ObservedObject -> let,
@EnvironmentObject -> @Environment(TabManager.self)); the footer no longer
plumbs modifierKeyMonitor, matching main. Main's stale-agent-icon fix
(#7740) merged clean into TerminalTabAgentIcon/Workspace+PanelLifecycle.
The file-length budget resolves to union-max per surviving path.
@coderabbitai coderabbitai Bot mentioned this pull request Jul 10, 2026
6 tasks done

This branch was successfully deployed

1 active deployment
Preview – cmux — 74f1c5ee Deployed Jul 10, 2026 by vercel[bot]
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.

Terminal tab agent icon goes stale: quit Claude, start Codex in same terminal, tab keeps the Claude icon

1 participant