@@ -31,6 +31,8 @@ class TabItem {
3131 case terminalIdle // muted teal - terminal at prompt
3232 case terminalActive // teal pulsing - terminal foreground process has activity
3333 case terminalError // red - terminal process exited with error
34+ case completedUnseen // vivid purple - Claude finished while tab unfocused
35+ case terminalCompletedUnseen // vivid teal - terminal finished while tab unfocused
3436 }
3537
3638 init ( surface: TerminalSurface , name: String , isClaude: Bool ) {
@@ -566,6 +568,7 @@ class DeckardWindowController: NSWindowController, NSSplitViewDelegate {
566568 } else {
567569 // Always clamp for safe array access, even during restore
568570 let safeIdx = max ( 0 , min ( project. selectedTabIndex, project. tabs. count - 1 ) )
571+ clearUnseenIfNeeded ( project. tabs [ safeIdx] )
569572 showTab ( project. tabs [ safeIdx] )
570573 }
571574
@@ -747,15 +750,33 @@ class DeckardWindowController: NSWindowController, NSSplitViewDelegate {
747750 project. selectedTabIndex = min ( idx, project. tabs. count - 1 )
748751 rebuildTabBar ( )
749752 rebuildSidebar ( )
753+ clearUnseenIfNeeded ( project. tabs [ project. selectedTabIndex] )
750754 showTab ( project. tabs [ project. selectedTabIndex] )
751755 }
752756 saveState ( )
753757 }
754758
759+ /// If the tab is in a completedUnseen state, revert to the normal idle state.
760+ func clearUnseenIfNeeded( _ tab: TabItem ) {
761+ switch tab. badgeState {
762+ case . completedUnseen:
763+ tab. badgeState = . waitingForInput
764+ rebuildSidebar ( )
765+ rebuildTabBar ( )
766+ case . terminalCompletedUnseen:
767+ tab. badgeState = . terminalIdle
768+ rebuildSidebar ( )
769+ rebuildTabBar ( )
770+ default :
771+ break
772+ }
773+ }
774+
755775 func selectTabInProject( at tabIndex: Int ) {
756776 guard let project = currentProject else { return }
757777 guard tabIndex >= 0 , tabIndex < project. tabs. count else { return }
758778 project. selectedTabIndex = tabIndex
779+ clearUnseenIfNeeded ( project. tabs [ tabIndex] )
759780 rebuildTabBar ( )
760781 showTab ( project. tabs [ tabIndex] )
761782 }
@@ -768,6 +789,7 @@ class DeckardWindowController: NSWindowController, NSSplitViewDelegate {
768789 guard tabIndex >= 0 , tabIndex < project. tabs. count else { return }
769790 guard tabIndex != project. selectedTabIndex else { return }
770791 project. selectedTabIndex = tabIndex
792+ clearUnseenIfNeeded ( project. tabs [ tabIndex] )
771793 showTab ( project. tabs [ tabIndex] )
772794 }
773795
@@ -915,7 +937,20 @@ class DeckardWindowController: NSWindowController, NSSplitViewDelegate {
915937 terminalActiveStreak [ tab. id] = newStreak
916938 let confirmedActive = newStreak >= 2
917939
918- let newBadge : TabItem . BadgeState = confirmedActive ? . terminalActive : . terminalIdle
940+ let newBadge : TabItem . BadgeState
941+ if confirmedActive {
942+ newBadge = . terminalActive
943+ } else if tab. badgeState == . terminalActive {
944+ // Transitioning from active to idle — check focus
945+ let focused = isTabFocused ( tab. id. uuidString)
946+ newBadge = focused ? . terminalIdle : . terminalCompletedUnseen
947+ } else if tab. badgeState == . terminalCompletedUnseen {
948+ // Stay unseen until tab is visited (cleared elsewhere)
949+ newBadge = . terminalCompletedUnseen
950+ } else {
951+ newBadge = . terminalIdle
952+ }
953+
919954 terminalActivity [ tab. id] = activity
920955 if tab. badgeState != newBadge {
921956 if newBadge == . terminalActive {
@@ -1049,6 +1084,24 @@ class DeckardWindowController: NSWindowController, NSSplitViewDelegate {
10491084 rebuildTabBar ( )
10501085 }
10511086
1087+ /// Like updateBadge, but substitutes completedUnseen/terminalCompletedUnseen
1088+ /// when the tab transitions to an idle state while unfocused.
1089+ func updateBadgeToIdleOrUnseen( forSurfaceId surfaceIdStr: String , isClaude: Bool ) {
1090+ guard let tab = tabForSurfaceId ( surfaceIdStr) else { return }
1091+ let wasBusy = isClaude
1092+ ? ( tab. badgeState == . thinking || tab. badgeState == . needsPermission)
1093+ : ( tab. badgeState == . terminalActive)
1094+ let focused = isTabFocused ( surfaceIdStr)
1095+ let idleState : TabItem . BadgeState = isClaude ? . waitingForInput : . terminalIdle
1096+ let unseenState : TabItem . BadgeState = isClaude ? . completedUnseen : . terminalCompletedUnseen
1097+ let newState = ( wasBusy && !focused) ? unseenState : idleState
1098+ DiagnosticLog . shared. log ( " badge " ,
1099+ " updateBadgeToIdleOrUnseen: surfaceId= \( surfaceIdStr) wasBusy= \( wasBusy) focused= \( focused) -> \( newState) " )
1100+ tab. badgeState = newState
1101+ rebuildSidebar ( )
1102+ rebuildTabBar ( )
1103+ }
1104+
10521105 func listTabInfo( ) -> [ TabInfo ] {
10531106 var result : [ TabInfo ] = [ ]
10541107 for project in projects {
0 commit comments