@@ -48,6 +48,9 @@ public partial class MainWindow : Window
4848 private readonly Dictionary < string , Border > _dormantSidebarItems = [ ] ;
4949 // Anchor for shift-click range selection in the sidebar.
5050 private string ? _selectionAnchorId ;
51+ // Group-tab notification indicators (badge + text), keyed by group id (or "__ALL__"
52+ // / GroupFilter.Ungrouped sentinels). Repopulated on every RebuildGroupStrip.
53+ private readonly Dictionary < string , ( Border badge , TextBlock badgeText ) > _groupTabIndicators = [ ] ;
5154
5255 private SqliteConnection ? _db ;
5356 private SearchService ? _searchService ;
@@ -95,9 +98,18 @@ public MainWindow()
9598 } ) ;
9699 _vm . SelectionChanged += ( ) => Dispatcher . Invoke ( UpdateSidebarActiveState ) ;
97100 // Re-filter the sidebar when a session's GroupId changes — otherwise the current
98- // filter view stays stale until the user clicks a different tab.
99- _vm . SessionMembershipChanged += ( ) => Dispatcher . Invoke ( RebuildSidebarOrder ) ;
100- _vm . Sessions . CollectionChanged += ( _ , _ ) => RecomputeWorktreeSiblings ( ) ;
101+ // filter view stays stale until the user clicks a different tab. Also refresh the
102+ // group-tab indicators since session-to-group membership just shifted.
103+ _vm . SessionMembershipChanged += ( ) => Dispatcher . Invoke ( ( ) =>
104+ {
105+ RebuildSidebarOrder ( ) ;
106+ UpdateGroupTabIndicators ( ) ;
107+ } ) ;
108+ _vm . Sessions . CollectionChanged += ( _ , _ ) =>
109+ {
110+ RecomputeWorktreeSiblings ( ) ;
111+ UpdateGroupTabIndicators ( ) ;
112+ } ;
101113
102114 Loaded += OnLoaded ;
103115 KeyDown += OnKeyDown ;
@@ -994,6 +1006,7 @@ static void UpdateGitText(TextBlock tb, SessionViewModel svm)
9941006 case nameof ( SessionViewModel . NeedsAttention ) :
9951007 alertBadge . Visibility = vm . NeedsAttention ? Visibility . Visible : Visibility . Collapsed ;
9961008 UpdateAlertBadge ( ) ;
1009+ UpdateGroupTabIndicators ( ) ;
9971010 break ;
9981011
9991012 case nameof ( SessionViewModel . IsWaitingForInput ) :
@@ -1014,6 +1027,7 @@ static void UpdateGitText(TextBlock tb, SessionViewModel svm)
10141027 {
10151028 statusDot . Visibility = Visibility . Collapsed ;
10161029 }
1030+ UpdateGroupTabIndicators ( ) ;
10171031 break ;
10181032
10191033 case nameof ( SessionViewModel . GitBranch ) :
@@ -1144,6 +1158,7 @@ private void UpdateGroupStripVisibility()
11441158 private void RebuildGroupStrip ( )
11451159 {
11461160 GroupStripPanel . Children . Clear ( ) ;
1161+ _groupTabIndicators . Clear ( ) ;
11471162 if ( _sessionManager . Groups . Count == 0 ) return ;
11481163
11491164 GroupStripPanel . Children . Add ( BuildGroupTab ( null , "All" , "▦" ) ) ;
@@ -1178,6 +1193,75 @@ private void RebuildGroupStrip()
11781193 GroupStripPanel . Children . Add ( addBtn ) ;
11791194
11801195 UpdateGroupStripActiveState ( ) ;
1196+ UpdateGroupTabIndicators ( ) ;
1197+ }
1198+
1199+ /// <summary>
1200+ /// Refreshes the small status indicator on each group tab. Priority: alert count
1201+ /// (pink badge with N) > tool-approval (orange dot) > input-required (green dot) >
1202+ /// hidden. The "All" tab aggregates every live session; "Ungrouped" aggregates only
1203+ /// sessions with an empty GroupId.
1204+ /// </summary>
1205+ private void UpdateGroupTabIndicators ( )
1206+ {
1207+ if ( _groupTabIndicators . Count == 0 ) return ;
1208+
1209+ var pink = new SolidColorBrush ( Color . FromRgb ( 0xf3 , 0x8b , 0xa8 ) ) ;
1210+ var orange = new SolidColorBrush ( Color . FromRgb ( 0xff , 0xb7 , 0x4d ) ) ;
1211+ var green = new SolidColorBrush ( Color . FromRgb ( 0xa6 , 0xe3 , 0xa1 ) ) ;
1212+ var inkOnLight = new SolidColorBrush ( Color . FromRgb ( 0x1e , 0x1e , 0x2e ) ) ;
1213+
1214+ foreach ( var ( key , ( badge , text ) ) in _groupTabIndicators )
1215+ {
1216+ IEnumerable < SessionViewModel > set = key switch
1217+ {
1218+ "__ALL__" => _vm . Sessions ,
1219+ _ when key == GroupFilter . Ungrouped =>
1220+ _vm . Sessions . Where ( s => string . IsNullOrEmpty ( s . GroupId ) ) ,
1221+ _ => _vm . Sessions . Where ( s => s . GroupId == key )
1222+ } ;
1223+
1224+ int alerts = 0 ;
1225+ bool anyApproval = false , anyInput = false ;
1226+ foreach ( var s in set )
1227+ {
1228+ if ( s . NeedsAttention ) alerts ++ ;
1229+ if ( s . IsWaitingForApproval ) anyApproval = true ;
1230+ if ( s . IsWaitingForInput ) anyInput = true ;
1231+ }
1232+
1233+ if ( alerts > 0 )
1234+ {
1235+ badge . Background = pink ;
1236+ badge . MinWidth = 14 ;
1237+ text . Text = alerts . ToString ( ) ;
1238+ text . Foreground = inkOnLight ;
1239+ badge . ToolTip = alerts == 1
1240+ ? "1 session needs attention"
1241+ : $ "{ alerts } sessions need attention";
1242+ badge . Visibility = Visibility . Visible ;
1243+ }
1244+ else if ( anyApproval )
1245+ {
1246+ badge . Background = orange ;
1247+ badge . MinWidth = 10 ;
1248+ text . Text = "" ;
1249+ badge . ToolTip = "Tool approval needed" ;
1250+ badge . Visibility = Visibility . Visible ;
1251+ }
1252+ else if ( anyInput )
1253+ {
1254+ badge . Background = green ;
1255+ badge . MinWidth = 10 ;
1256+ text . Text = "" ;
1257+ badge . ToolTip = "Waiting for input" ;
1258+ badge . Visibility = Visibility . Visible ;
1259+ }
1260+ else
1261+ {
1262+ badge . Visibility = Visibility . Collapsed ;
1263+ }
1264+ }
11811265 }
11821266
11831267 private static string GroupInitials ( string name )
@@ -1207,7 +1291,9 @@ private Border BuildGroupTab(string? groupId, string fullName, string label)
12071291 Tag = "group:" + ( groupId ?? "__ALL__" ) ,
12081292 Height = 36
12091293 } ;
1210- border . Child = new TextBlock
1294+
1295+ var grid = new Grid ( ) ;
1296+ var labelText = new TextBlock
12111297 {
12121298 Text = label ,
12131299 Foreground = new SolidColorBrush ( Color . FromRgb ( 0xa6 , 0xad , 0xc8 ) ) ,
@@ -1216,6 +1302,38 @@ private Border BuildGroupTab(string? groupId, string fullName, string label)
12161302 HorizontalAlignment = HorizontalAlignment . Center ,
12171303 VerticalAlignment = VerticalAlignment . Center
12181304 } ;
1305+ grid . Children . Add ( labelText ) ;
1306+
1307+ // Notification indicator: pink badge with count when sessions in this group have
1308+ // NeedsAttention; orange/green dot when only waiting for approval/input.
1309+ // Hidden when the group has no active state. UpdateGroupTabIndicators recomputes it.
1310+ var indicatorText = new TextBlock
1311+ {
1312+ Text = "" ,
1313+ Foreground = new SolidColorBrush ( Color . FromRgb ( 0x1e , 0x1e , 0x2e ) ) ,
1314+ FontSize = 8 ,
1315+ FontWeight = FontWeights . Bold ,
1316+ HorizontalAlignment = HorizontalAlignment . Center ,
1317+ VerticalAlignment = VerticalAlignment . Center
1318+ } ;
1319+ var indicator = new Border
1320+ {
1321+ CornerRadius = new CornerRadius ( 7 ) ,
1322+ MinWidth = 10 ,
1323+ MinHeight = 10 ,
1324+ Padding = new Thickness ( 3 , 0 , 3 , 0 ) ,
1325+ HorizontalAlignment = HorizontalAlignment . Right ,
1326+ VerticalAlignment = VerticalAlignment . Top ,
1327+ Margin = new Thickness ( 0 , 3 , 4 , 0 ) ,
1328+ Visibility = Visibility . Collapsed ,
1329+ Child = indicatorText ,
1330+ IsHitTestVisible = false
1331+ } ;
1332+ grid . Children . Add ( indicator ) ;
1333+ border . Child = grid ;
1334+
1335+ _groupTabIndicators [ groupId ?? "__ALL__" ] = ( indicator , indicatorText ) ;
1336+
12191337 border . MouseLeftButtonDown += ( _ , _ ) =>
12201338 {
12211339 _vm . ActiveGroupId = groupId ;
0 commit comments