Skip to content

Commit ec08f91

Browse files
AThraenclaude
andcommitted
feat(ui): hover-to-reveal sidebar action icons + toolbar close (#29)
Per-session action icons are now consolidated. The sidebar row stack drops from 6 buttons to 3 (➕ 💤 ✕), governed by a new SidebarActionIconsMode setting (OnHover default / Always / Hidden). OnHover keeps the panel laid out (no text reflow on hover) but transparent and non-interactive until the row is hovered. The terminal toolbar gains its own ✕ close so it's a self-sufficient action surface. The actions removed from the sidebar — Rename, Open in Explorer, Open PowerShell here — move to the right-click context menu so they aren't lost; rename uses the same in-place editor as double-click via a captured Action stored in _sidebarRenameActions. Settings UI gets a new "Sidebar action icons" dropdown in Appearance. Live changes apply immediately to every row through _sidebarActionPanels without a sidebar rebuild. Deferred to a separate change: settings window reorganization and main toolbar audit (both mentioned in #29). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 902033a commit ec08f91

5 files changed

Lines changed: 154 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ Persisted in `state.json`. Key settings:
219219
- `AutoResumeClaude` — when restoring, append `--resume <sessionId>` to claude commands so the prior conversation is picked up. Toggle off if you want fresh sessions on restart.
220220
- `ShowGitBranch` — show `⎇ branch` in sidebar
221221
- `ShowTerminalStatusDot` — show status dot in terminal toolbar
222+
- `SidebarActionIconsMode``OnHover` (default) / `Always` / `Hidden`. Controls the per-row `➕ 💤 ✕` button stack in the sidebar. `Hidden` collapses the panel and reclaims the horizontal space; `OnHover` keeps the panel laid out (no text shift on hover) but transparent + non-interactive until the row is hovered. Rename / Open in Explorer / Open PowerShell here remain reachable via the right-click context menu in all modes, and the terminal toolbar's `` is unconditional.
222223
- `SearchCollapseAfterNavigate` — auto-close search after clicking result
223224
- `MaxSearchResults` — FTS5 result limit (default 100)
224225
- `DefaultWorkingFolder` / `DefaultCommand` — pre-fill new session dialog

src/CodeShellManager/MainWindow.xaml.cs

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public partial class MainWindow : Window
6363
WpfButton drawerCopyBtn,
6464
WpfButton drawerSendBtn)> _runControls = new();
6565
private readonly Dictionary<string, string> _drawerItemBySession = new();
66+
// Per-session sidebar action button panels — kept so SettingsButton_Click
67+
// can flip every row to a new SidebarActionIconsMode without rebuilding sidebar items.
68+
private readonly Dictionary<string, StackPanel> _sidebarActionPanels = new();
69+
// Per-session rename trigger — captured from BuildSidebarItem so the context menu's
70+
// Rename action can invoke the same in-place editor as the double-click handler.
71+
private readonly Dictionary<string, Action> _sidebarRenameActions = new();
6672
// Anchor for shift-click range selection in the sidebar.
6773
private string? _selectionAnchorId;
6874
// Group-tab notification indicators (badge + text), keyed by group id (or "__ALL__"
@@ -1184,29 +1190,30 @@ static void UpdateGitText(TextBlock tb, SessionViewModel svm)
11841190
};
11851191
Grid.SetColumn(statusDot, 2);
11861192

1187-
// Action buttons
1193+
// Action buttons — reduced set (#29). Secondary actions (Open in Explorer, PowerShell,
1194+
// Rename) live on the right-click context menu instead. The icons are surfaced based
1195+
// on AppSettings.SidebarActionIconsMode (default OnHover) so the sidebar stays calm
1196+
// unless the user is actively reaching for an action.
11881197
var btnPanel = new StackPanel
11891198
{
11901199
Orientation = Orientation.Vertical,
11911200
Margin = new Thickness(0, 4, 4, 4),
11921201
VerticalAlignment = VerticalAlignment.Center
11931202
};
11941203

1195-
var exploreBtn = MakeMiniButton("📁", "Open in Explorer", () => vm.OpenInExplorerCommand.Execute(null));
1196-
var psBtn = MakeMiniButton(">_", "Open PowerShell here", () => LaunchPowerShellInFolder(vm.WorkingFolder, vm.GroupId));
1197-
var spawnBtn = MakeMiniButton("➕", "New session here (inherits group + profile)",
1204+
var spawnBtn = MakeMiniButton("➕", "New session here (inherits group + profile)",
11981205
() => OpenNewSessionDialogFromParent(vm));
1199-
var renameBtn = MakeMiniButton("✏", "Rename session", StartRename);
1200-
var sleepBtn = MakeMiniButton("💤", "Sleep session (keep it but stop the terminal)", () => SleepSession(vm));
1201-
var closeBtn = MakeMiniButton("✕", "Close session", () => vm.CloseCommand.Execute(null));
1206+
var sleepBtn = MakeMiniButton("💤", "Sleep session (keep it but stop the terminal)", () => SleepSession(vm));
1207+
var closeBtn = MakeMiniButton("✕", "Close session", () => vm.CloseCommand.Execute(null));
12021208

1203-
btnPanel.Children.Add(exploreBtn);
1204-
btnPanel.Children.Add(psBtn);
12051209
btnPanel.Children.Add(spawnBtn);
1206-
btnPanel.Children.Add(renameBtn);
12071210
btnPanel.Children.Add(sleepBtn);
12081211
btnPanel.Children.Add(closeBtn);
12091212

1213+
_sidebarActionPanels[vm.Id] = btnPanel;
1214+
_sidebarRenameActions[vm.Id] = StartRename;
1215+
ApplyActionIconsMode(btnPanel, container, _vm.Settings.SidebarActionIconsMode, isHovered: false);
1216+
12101217
Grid.SetColumn(textPanel, 1);
12111218
Grid.SetColumn(btnPanel, 3);
12121219
inner.Children.Add(stripe);
@@ -1283,14 +1290,18 @@ static void UpdateGitText(TextBlock tb, SessionViewModel svm)
12831290
// Hover effect — must not clobber multi-select tint. Selected-but-not-active items
12841291
// keep their blue background on hover and on mouse leave; only plain, unselected,
12851292
// non-active items show the muted hover background and clear to transparent.
1293+
// Also drives the OnHover SidebarActionIconsMode: action buttons fade in on enter,
1294+
// out on leave. Hidden and Always modes ignore the hover transition.
12861295
container.MouseEnter += (_, _) =>
12871296
{
1297+
ApplyActionIconsMode(btnPanel, container, _vm.Settings.SidebarActionIconsMode, isHovered: true);
12881298
if (vm.Id == _vm.ActiveSession?.Id) return;
12891299
if (_vm.IsSelected(vm.Id)) return;
12901300
container.Background = new SolidColorBrush(Color.FromRgb(0x31, 0x32, 0x44));
12911301
};
12921302
container.MouseLeave += (_, _) =>
12931303
{
1304+
ApplyActionIconsMode(btnPanel, container, _vm.Settings.SidebarActionIconsMode, isHovered: false);
12941305
if (vm.Id == _vm.ActiveSession?.Id) return;
12951306
container.Background = _vm.IsSelected(vm.Id)
12961307
? new SolidColorBrush(Color.FromArgb(0x55, 0x89, 0xb4, 0xfa))
@@ -1391,6 +1402,37 @@ private static WpfButton MakeMiniButton(string icon, string tooltip, Action onCl
13911402
return btn;
13921403
}
13931404

1405+
/// <summary>
1406+
/// Applies the current <see cref="Models.SidebarActionIconsMode"/> to a single sidebar
1407+
/// row's action-button stack. Hidden collapses the panel entirely (the row reclaims the
1408+
/// horizontal space). OnHover keeps the panel laid out but transparent + non-interactive
1409+
/// until the row is hovered. Always shows it at full opacity. Called from BuildSidebarItem
1410+
/// at construction, from the row's MouseEnter/Leave handlers, and after a settings save.
1411+
/// </summary>
1412+
private static void ApplyActionIconsMode(StackPanel btnPanel, Border container,
1413+
Models.SidebarActionIconsMode mode, bool isHovered)
1414+
{
1415+
switch (mode)
1416+
{
1417+
case Models.SidebarActionIconsMode.Hidden:
1418+
btnPanel.Visibility = Visibility.Collapsed;
1419+
btnPanel.IsHitTestVisible = false;
1420+
btnPanel.Opacity = 0;
1421+
break;
1422+
case Models.SidebarActionIconsMode.Always:
1423+
btnPanel.Visibility = Visibility.Visible;
1424+
btnPanel.IsHitTestVisible = true;
1425+
btnPanel.Opacity = 1;
1426+
break;
1427+
case Models.SidebarActionIconsMode.OnHover:
1428+
default:
1429+
btnPanel.Visibility = Visibility.Visible; // reserves layout space
1430+
btnPanel.Opacity = isHovered ? 1 : 0;
1431+
btnPanel.IsHitTestVisible = isHovered;
1432+
break;
1433+
}
1434+
}
1435+
13941436
private void UpdateSidebarActiveState()
13951437
{
13961438
foreach (Border item in SidebarSessionList.Children)
@@ -2124,6 +2166,29 @@ private System.Windows.Controls.ContextMenu BuildSessionContextMenu(SessionViewM
21242166
// Spawn-near-parent + worktree actions — single-target only
21252167
if (!isMulti)
21262168
{
2169+
// Rename — in-place editor inside the sidebar row (same path as double-click).
2170+
// Stored in _sidebarRenameActions when the sidebar row is built.
2171+
if (_sidebarRenameActions.TryGetValue(vm.Id, out var renameAction))
2172+
{
2173+
var renameItem = new System.Windows.Controls.MenuItem { Header = "Rename session" };
2174+
renameItem.Click += (_, _) => renameAction();
2175+
menu.Items.Add(renameItem);
2176+
}
2177+
2178+
// Folder actions — only when there's a local working folder to open.
2179+
if (!vm.Session.IsRemote && !string.IsNullOrEmpty(vm.Session.WorkingFolder))
2180+
{
2181+
var explorerItem = new System.Windows.Controls.MenuItem { Header = "Open in Explorer" };
2182+
explorerItem.Click += (_, _) => vm.OpenInExplorerCommand.Execute(null);
2183+
menu.Items.Add(explorerItem);
2184+
2185+
var psItem = new System.Windows.Controls.MenuItem { Header = "Open PowerShell here" };
2186+
psItem.Click += (_, _) => LaunchPowerShellInFolder(vm.WorkingFolder, vm.GroupId);
2187+
menu.Items.Add(psItem);
2188+
}
2189+
2190+
menu.Items.Add(new System.Windows.Controls.Separator());
2191+
21272192
var dupItem = new System.Windows.Controls.MenuItem { Header = "Duplicate session" };
21282193
dupItem.Click += async (_, _) => await DuplicateSessionAsync(vm);
21292194
menu.Items.Add(dupItem);
@@ -3050,6 +3115,25 @@ private Border BuildTerminalWrapper(SessionViewModel vm, WebView2 webView)
30503115
};
30513116
sleepBtn.Click += (_, _) => SleepSession(vm);
30523117

3118+
// Close button — tied to the same path as the sidebar ✕ (vm.CloseCommand).
3119+
// Sits at the far right of the toolbar so the terminal toolbar is a complete,
3120+
// canonical home for per-session actions (#29). Right margin nudges it slightly
3121+
// inside the toolbar padding so the ✕ doesn't kiss the toolbar edge.
3122+
var closeBtn = new WpfButton
3123+
{
3124+
Content = "✕",
3125+
ToolTip = "Close session",
3126+
Background = Brushes.Transparent,
3127+
BorderThickness = new Thickness(0),
3128+
Foreground = new SolidColorBrush(Color.FromRgb(0xa6, 0xad, 0xc8)),
3129+
FontSize = 12,
3130+
Cursor = System.Windows.Input.Cursors.Hand,
3131+
Padding = new Thickness(4, 2, 4, 2),
3132+
Margin = new Thickness(0)
3133+
};
3134+
closeBtn.Click += (_, _) => vm.CloseCommand.Execute(null);
3135+
3136+
DockPanel.SetDock(closeBtn, Dock.Right);
30533137
DockPanel.SetDock(termStatusDot, Dock.Right);
30543138
DockPanel.SetDock(explorerBtn, Dock.Right);
30553139
DockPanel.SetDock(toolbarPsBtn, Dock.Right);
@@ -3059,6 +3143,9 @@ private Border BuildTerminalWrapper(SessionViewModel vm, WebView2 webView)
30593143
DockPanel.SetDock(playBtn, Dock.Right);
30603144
DockPanel.SetDock(claudeBadge, Dock.Left);
30613145
DockPanel.SetDock(titleBlock, Dock.Left);
3146+
// Dock.Right children are stacked right-to-left in declaration order, so this
3147+
// puts closeBtn at the far right edge, then termStatusDot to its left, etc.
3148+
toolbarContent.Children.Add(closeBtn);
30623149
toolbarContent.Children.Add(termStatusDot);
30633150
toolbarContent.Children.Add(explorerBtn);
30643151
toolbarContent.Children.Add(toolbarPsBtn);
@@ -3295,6 +3382,8 @@ private void OnSessionVmClosed(SessionViewModel vm)
32953382
}
32963383
_runControls.Remove(vm.Id);
32973384
_drawerItemBySession.Remove(vm.Id);
3385+
_sidebarActionPanels.Remove(vm.Id);
3386+
_sidebarRenameActions.Remove(vm.Id);
32983387
if (_selectionAnchorId == vm.Id) _selectionAnchorId = null;
32993388
_sessionManager.RemoveSession(vm.Id);
33003389
RefreshTerminalLayout();
@@ -3331,6 +3420,8 @@ private void SleepSession(SessionViewModel vm)
33313420
}
33323421
_runControls.Remove(vm.Id);
33333422
_drawerItemBySession.Remove(vm.Id);
3423+
_sidebarActionPanels.Remove(vm.Id);
3424+
_sidebarRenameActions.Remove(vm.Id);
33343425

33353426
// Remove the VM directly — bypass CloseRequested so the ShellSession is
33363427
// NOT removed from the SessionManager (we want to keep it for wake-up).
@@ -3754,6 +3845,7 @@ private void SettingsButton_Click(object sender, RoutedEventArgs e)
37543845
_vm.Settings.ShowGitBranch = edited.ShowGitBranch;
37553846
_vm.Settings.ShowGroupsTab = edited.ShowGroupsTab;
37563847
_vm.Settings.GroupDisplayMode = edited.GroupDisplayMode;
3848+
_vm.Settings.SidebarActionIconsMode = edited.SidebarActionIconsMode;
37573849
_vm.Settings.ShowWorktreeClusters = edited.ShowWorktreeClusters;
37583850
_vm.Settings.SearchCollapseAfterNavigate = edited.SearchCollapseAfterNavigate;
37593851

@@ -3777,6 +3869,17 @@ private void SettingsButton_Click(object sender, RoutedEventArgs e)
37773869
foreach (var vm in _vm.Sessions)
37783870
vm.Bridge?.ApplyFontSettings(_vm.Settings);
37793871

3872+
// Push the action-icons mode to every live sidebar row. Hovered state is
3873+
// recomputed via IsMouseOver so the change is visible immediately without
3874+
// requiring the cursor to leave + re-enter the row.
3875+
foreach (var (id, panel) in _sidebarActionPanels)
3876+
{
3877+
if (_sessionUi.TryGetValue(id, out var ui))
3878+
ApplyActionIconsMode(panel, ui.sidebarItem,
3879+
_vm.Settings.SidebarActionIconsMode,
3880+
isHovered: ui.sidebarItem.IsMouseOver);
3881+
}
3882+
37803883
UpdateGroupStripVisibility();
37813884
RebuildSidebarOrder();
37823885
}

src/CodeShellManager/Models/AppState.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ public enum GroupDisplayMode
1515
InlineHeaders
1616
}
1717

18+
/// <summary>
19+
/// How per-session action icons (close / sleep / spawn) are surfaced on each sidebar
20+
/// row. OnHover (default) keeps the sidebar visually calm — icons fade in only while
21+
/// the row is hovered. Always shows them at all times (the legacy behaviour). Hidden
22+
/// removes them entirely; users rely on the right-click context menu or the terminal
23+
/// toolbar instead. The terminal toolbar always shows its own icons regardless.
24+
/// </summary>
25+
public enum SidebarActionIconsMode
26+
{
27+
Hidden,
28+
OnHover,
29+
Always
30+
}
31+
1832
public class AppSettings
1933
{
2034
public bool AutoRestoreSessions { get; set; } = true;
@@ -59,6 +73,8 @@ public class AppSettings
5973
public int MaxSearchResults { get; set; } = 100;
6074
public bool ShowTerminalStatusDot { get; set; } = true;
6175
public bool ImportWindowsTerminalProfiles { get; set; } = false;
76+
/// <summary>How per-row action icons are surfaced in the sidebar. See <see cref="Models.SidebarActionIconsMode"/>.</summary>
77+
public SidebarActionIconsMode SidebarActionIconsMode { get; set; } = SidebarActionIconsMode.OnHover;
6278

6379
// Storage
6480
public bool IndexTerminalOutput { get; set; } = true;

src/CodeShellManager/Views/SettingsWindow.xaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@
219219
<StackPanel Margin="0,8,0,0">
220220
<CheckBox x:Name="ShowGitBranchCheck" Content="Show git branch in sidebar"/>
221221
<CheckBox x:Name="ShowTerminalStatusDotCheck" Content="Show terminal status indicator"/>
222+
<StackPanel Margin="0,4,0,4">
223+
<TextBlock Text="Sidebar action icons" Style="{StaticResource Label}"/>
224+
<ComboBox x:Name="SidebarActionIconsModeCombo">
225+
<ComboBoxItem Content="Show on hover (default)" Tag="OnHover"/>
226+
<ComboBoxItem Content="Always show" Tag="Always"/>
227+
<ComboBoxItem Content="Hide (use right-click instead)" Tag="Hidden"/>
228+
</ComboBox>
229+
<TextBlock Text="The small ➕ / 💤 / ✕ buttons on each sidebar row. Hidden moves all per-session actions to the right-click menu and the terminal toolbar; rename still works via double-click."
230+
Foreground="#6c7086" FontSize="10" Margin="0,4,0,0" TextWrapping="Wrap"/>
231+
</StackPanel>
222232
<StackPanel Margin="0,4,0,4">
223233
<TextBlock Text="Session group display" Style="{StaticResource Label}"/>
224234
<ComboBox x:Name="GroupDisplayModeCombo">

src/CodeShellManager/Views/SettingsWindow.xaml.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public SettingsWindow(AppSettings current, SearchService? searchService = null)
3333
ShowGitBranch = current.ShowGitBranch,
3434
ShowGroupsTab = current.ShowGroupsTab,
3535
GroupDisplayMode = current.GroupDisplayMode,
36+
SidebarActionIconsMode = current.SidebarActionIconsMode,
3637
ShowWorktreeClusters = current.ShowWorktreeClusters,
3738
SearchCollapseAfterNavigate = current.SearchCollapseAfterNavigate,
3839
Theme = current.Theme,
@@ -70,6 +71,16 @@ public SettingsWindow(AppSettings current, SearchService? searchService = null)
7071
}
7172
if (GroupDisplayModeCombo.SelectedIndex < 0)
7273
GroupDisplayModeCombo.SelectedIndex = 1; // FilterStrip default
74+
foreach (ComboBoxItem item in SidebarActionIconsModeCombo.Items)
75+
{
76+
if (item.Tag?.ToString() == _edited.SidebarActionIconsMode.ToString())
77+
{
78+
SidebarActionIconsModeCombo.SelectedItem = item;
79+
break;
80+
}
81+
}
82+
if (SidebarActionIconsModeCombo.SelectedIndex < 0)
83+
SidebarActionIconsModeCombo.SelectedIndex = 0; // OnHover default
7384
ImportWindowsTerminalProfilesCheck.IsChecked = _edited.ImportWindowsTerminalProfiles;
7485
SearchCollapseAfterNavigateCheck.IsChecked = _edited.SearchCollapseAfterNavigate;
7586
MaxSearchResultsBox.Text = _edited.MaxSearchResults.ToString();
@@ -141,6 +152,9 @@ private void Save_Click(object sender, RoutedEventArgs e)
141152
// Keep the legacy flag in sync so a downgrade to an older build still respects None.
142153
_edited.ShowGroupsTab = newMode != Models.GroupDisplayMode.None;
143154
}
155+
var iconsModeTag = (SidebarActionIconsModeCombo.SelectedItem as ComboBoxItem)?.Tag?.ToString();
156+
if (System.Enum.TryParse<Models.SidebarActionIconsMode>(iconsModeTag, out var newIconsMode))
157+
_edited.SidebarActionIconsMode = newIconsMode;
144158
_edited.ImportWindowsTerminalProfiles = ImportWindowsTerminalProfilesCheck.IsChecked == true;
145159
_edited.SearchCollapseAfterNavigate = SearchCollapseAfterNavigateCheck.IsChecked == true;
146160
_edited.AnthropicApiKey = ApiKeyBox.Password;

0 commit comments

Comments
 (0)