Skip to content

Commit 403fee5

Browse files
mortenasloclaude
andcommitted
feat: group-tab notification indicators (alert count, approval, input)
Each tab in the group strip now carries a small status pip in its top- right corner that aggregates the state of the sessions inside the group: - Pink badge with the alert count when any session has NeedsAttention. - Orange dot when at least one session is waiting for tool approval. - Green dot when at least one session is waiting for input. - Hidden when the group has no active state. Priority is alert count > approval > input — same hierarchy the per- session sidebar dot uses. The "All" tab aggregates every live session; "Ungrouped" only those with an empty GroupId; named groups match by id. Refresh is wired to NeedsAttention/IsWaitingForInput/IsWaitingForApproval property changes per session, to Sessions.CollectionChanged (add/close/ sleep/wake), and to SessionMembershipChanged (Add-to-group / Remove-from- group). RebuildGroupStrip clears the indicator dictionary so each rebuild gets fresh references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 922cf50 commit 403fee5

1 file changed

Lines changed: 122 additions & 4 deletions

File tree

src/CodeShellManager/MainWindow.xaml.cs

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)