feat: session groups + per-branch worktrees#28
Conversation
Adds two related sidebar features: - Categories: a vertical group-tab strip to the left of the session list that filters sessions by user-defined group. Shift/Ctrl-click on session items to multi-select, then right-click → "Add to group ▸". Strip auto- hides when no groups exist or when ShowGroupsTab is off. The legacy auto- created "Default" group is silently dropped on first load after upgrade. - Worktrees: right-click a local git session → "New worktree from this branch…" creates a sibling worktree (new or existing branch) and launches a new session in it cloning the source session's command + profile. When 2+ live sessions share a repo root, both show a "📁 repo ⎇ branch" subtitle so the relationship is visible inside any category. Also fixes the sidebar header "+" button alignment and right-edge clipping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds on the worktree + groups foundation to close issues #23 and #27: - New Session dialog auto-detects sibling worktrees (`git worktree list --porcelain`) when the chosen folder is in a multi-worktree repo and shows a "Also start sessions in:" multi-select panel. Selected siblings launch with the same command + group + profile as the primary and cluster immediately after it in the sidebar. - Per-session ➕ button in the sidebar action panel + "Duplicate session" / "New session here…" / "New session in sibling worktree ▸" entries in the right-click menu. The sibling-worktree submenu is populated on demand and skips worktrees already open as sessions. - Ctrl+Shift+T duplicates the active session (browser-style). - New sessions created from a parent inherit the parent's GroupId and profile overrides, and land at parentIndex+1 in both SessionManager and the VM Sessions collection (added afterSessionId parameter on CreateSession; RegisterSession mirrors the index). - Worktree siblings share an accent color: SessionViewModel.AccentColor now keys on RepoRoot instead of WorkingFolder when the session is in a git repo, so siblings cluster visually. Sidebar stripe + active-ring repaint when RepoRoot resolves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up bugs: - The sidebar hover handlers reset the background to Transparent on MouseLeave without checking the selection set, so the multi-select blue tint disappeared as soon as the cursor moved away — the only surviving signal that a session was selected was the right-click count suffix. MouseEnter/MouseLeave now respect IsSelected and keep the tint when not actively hovering. - AssignSessionsToGroup didn't notify the view layer, so the current group filter stayed stale until the user clicked a different tab. Added a SessionMembershipChanged event on MainViewModel, fired from AssignSessionsToGroup; MainWindow subscribes and calls RebuildSidebarOrder so the filter updates immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- The active session's #313244 background got drowned out as soon as blue-tinted multi-select items appeared next to it. The active item now carries a 2px accent-colored border (the same color as the active terminal ring) AND uses the lighter Catppuccin Surface2 (#585b70) for its background when not also selected. Border thickness is constant (with a transparent placeholder on inactive items) so layout doesn't shift. Multi-selected and active states compose: selected bg + accent ring. - InputBoxDialog (used for new/rename group) had a fixed 160px Height that clipped the bottom edge of the OK/Cancel buttons on some DPI setups. Switched to SizeToContent="Height" with a MinHeight so the dialog auto-fits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Adds two ways to organize the group strip: - Drag a group tab vertically within the strip to reorder it. "All" and "Ungrouped" stay pinned at the top (not draggable, not valid drop targets); only user groups participate. The Drop handler resolves the new index from the cursor position relative to the existing user-group tabs, ignoring the fixed pseudo-tabs and the "+" footer. - Right-click any user group tab → "Move up" / "Move down". Items are disabled at the edges (first group can't move up; last can't move down). Backend: SessionManager.MoveGroup mutates the in-memory list and renumbers SortOrder on every group so the new order survives serialization to state.json. MainViewModel.MoveGroup wraps it and triggers SaveStateAsync. The existing GroupsChanged subscription in MainWindow rebuilds the strip in the new order automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root cause of "no visible grouping with two worktrees" was the sibling-detection key. GetRepoRootAsync used `git rev-parse --show- toplevel`, which returns each worktree's own folder, not the shared repo identity — so two worktrees of the same repo had different RepoRoot strings and never matched as siblings (no shared accent color, no subtitle, no cluster). Now it uses `git rev-parse --git-common-dir` and strips the trailing .git segment to derive a canonical repo path that's identical across every worktree. Forward-slash-normalized for stable comparison on Windows. On top of the fix, worktree siblings now render under an explicit labelled header in the sidebar: a thin "📁 repoName · N" banner tinted with the shared accent color, sitting above each run of adjacent visible siblings. The header count reflects what's visible in the current category filter (siblings split across categories don't pull each other into view). Toggleable via a new ShowWorktreeClusters setting (default on); when off, only the existing implicit signals (shared stripe color + subtitle line) remain. RecomputeWorktreeSiblings now triggers RebuildSidebarOrder when sibling flags actually change, so clusters form/dissolve live as RepoRoot resolves or sessions come and go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups so the worktree clustering and category filter behave the way the user expects: - Auto-cluster reorder: when a session is added/removed or a session's RepoRoot resolves, RecomputeWorktreeSiblings now also pulls every session sharing a RepoRoot next to its first-seen anchor. Siblings always cluster — no longer dependent on the user creating them via the worktree dialog (which alone uses afterSessionId). The reorder is stable: first-occurrence order is preserved between clusters and for solo sessions, so unrelated sessions never get shuffled past each other. - The Sessions.CollectionChanged subscription now filters Action=Move out of the recompute path so the in-place reorder doesn't recurse, and a user drag-to-reorder isn't immediately undone. - New sessions launched from the toolbar/sidebar "+" while a real group filter is active now inherit that group automatically. Group resolution priority: explicit dialog selection > parent session's group > active filter group (skipping All/Ungrouped) > ungrouped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group tabs are now drop targets for session-id drags: - Drop a session onto a named group tab → that session's GroupId is set to the target group. If the dragged session is part of a multi- select set, the whole selection moves to the group (uses the existing ResolveActionTargets). - Drop onto "Ungrouped" → clears GroupId for the target session(s). - The "All" tab is a view, not a real group, so it doesn't accept drops. - DragEnter tints the target tab with a stronger blue; DragLeave/Drop restore via UpdateGroupStripActiveState. Payload disambiguation: session drags use the raw vm.Id, group reorder drags use "group:<id>". The new IsSessionDragPayload helper routes each to its correct handler — group tabs only respond to session payloads, the GroupStripPanel only responds to "group:" payloads. Hardened the SidebarSessionList Drop too so a stray group-tab drop on the session list no longer shows a misleading "drop here" cursor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a GroupDisplayMode enum that replaces the legacy ShowGroupsTab boolean as the authoritative grouping UI selector. Three modes: - None — flat session list, no group UI surfaced at all - FilterStrip (default) — the existing vertical tab strip - InlineHeaders — collapsible group sections inline in the sidebar (the workflow described in #24) InlineHeaders rendering: - Each user group renders as a header bar with a ▶/▼ caret, the group name, and a member count. Click toggles expand/collapse via the existing SessionGroup.IsExpanded field (persisted to state.json). - Sessions without a GroupId go under an implicit "Ungrouped" header at the top. The Ungrouped expand state is per-process (no need to persist a synthetic field). - Worktree cluster headers still form WITHIN each group section — the count reflects visible siblings under that header, so split-across- groups doesn't show a misleading total. - The filter strip is auto-hidden in this mode (mode-driven, not count-driven). Drag-and-drop in inline mode: - Drag a session onto a group header → assigns the dragged session (plus the rest of the multi-select set, if any) to that group. - Drag a group header onto another → reorders groups. Drop directly on the sidebar background also reorders (position resolved by Y). - Existing session-reorder drag on session items still works (constrained to within a group section by RebuildSidebarOrder). - Group headers use MouseLeftButtonUp + a dragPending flag for the expand/collapse toggle, so a drag operation doesn't also fire the toggle. Settings UI: replaces the on/off checkbox with a 3-way ComboBox. Saves keep ShowGroupsTab in sync (= mode != None) so downgrades to older builds still respect the "no strip" choice. Migration: on load, if state has the legacy ShowGroupsTab=false AND GroupDisplayMode is still at the default FilterStrip, treat as None. Mode changes reset ActiveGroupId since the filter only applies in FilterStrip mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NewWorktreeDialog's ComboBox style only set Background/Foreground but didn't override the ControlTemplate, so the dropdown popup fell back to the system (light) theme — white text on a white popup. Adopted the full dark ControlTemplate that NewSessionDialog and SettingsWindow already use: dark popup Border with Catppuccin Surface0/1 fills, hover/selected/highlighted triggers on items, focus-blue trigger on the selection box when the dropdown is open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In inline-headers (and None) mode there's no ActiveGroupId — the group filter only exists in FilterStrip mode — so the previous "inherit active filter" fallback never fired for users on the new modes. Added a final fallback: if no parent and no active filter, inherit the group of the currently active session. Result: in inline mode, clicking + while a session in "ProjectA" is active lands the new session in "ProjectA" automatically. Resolution priority is now: 1. Explicit dialog selection (currently unused) 2. Parent session's group (spawn-near-parent flows) 3. Active filter (FilterStrip mode) 4. Active session's group (InlineHeaders/None mode) 5. Ungrouped Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In inline-headers mode, dragging a session from one group section into another now updates the session's GroupId to the target section (instead of snapping back). The drop position resolves both: - The target group: which section the cursor fell into (Ungrouped / any user group). - The _vm.Sessions index: the insertion point within that section, computed against per-section session bounds (cluster / dormant / groupheader items are skipped, only real session items count). If the drop falls past every session in the target section, the dragged session lands at the section's tail (just after the last existing member of that group in _vm.Sessions). Same-section drops keep the existing within-section reorder semantics. Multi-select: when the dragged session is part of a selection, the cross-section reassign applies to the whole set (mirroring the existing header-drop UX). The primary dragged session is moved to the resolved index; the rest keep their _vm.Sessions positions, which preserves their within-section order in the new group. Updates the PR's "out of scope" note — cross-section drag is now in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Property reads aren't narrowed through !string.IsNullOrEmpty, so the ternary kept typing s.RepoRoot as string?. Extracted a local ClusterKey helper that uses a pattern-matched local to narrow cleanly, which also removes the duplicated ternary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds sidebar organization and git-worktree ergonomics to make large session lists manageable, plus “spawn near parent” creation flows that preserve visual grouping and ordering.
Changes:
- Adds session groups with a new
GroupDisplayModesetting (None / FilterStrip / InlineHeaders) and group CRUD + drag/drop assignment/reorder. - Adds git worktree awareness: worktree listing + creation flow, sibling detection, shared accent color, optional cluster headers, and New Session multi-add into sibling worktrees.
- Adds “spawn near parent” behaviors: per-session ➕, duplicate session (
Ctrl+Shift+T), and insert-after-parent ordering with group/profile inheritance.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/CodeShellManager/Views/SettingsWindow.xaml.cs | Wires new settings fields into Settings UI and persistence. |
| src/CodeShellManager/Views/SettingsWindow.xaml | Adds group display mode combobox + worktree cluster checkbox. |
| src/CodeShellManager/Views/NewWorktreeDialog.xaml.cs | Implements dialog logic/validation for creating a git worktree. |
| src/CodeShellManager/Views/NewWorktreeDialog.xaml | Adds dark-themed worktree creation dialog UI. |
| src/CodeShellManager/Views/NewSessionDialog.xaml.cs | Adds sibling worktree probing UI + prefill support for spawn/duplicate flows. |
| src/CodeShellManager/Views/NewSessionDialog.xaml | Adds “Also start sessions in sibling worktrees” panel. |
| src/CodeShellManager/Views/InputBoxDialog.xaml.cs | Adds a small reusable modal input dialog helper. |
| src/CodeShellManager/Views/InputBoxDialog.xaml | Adds dark-themed input dialog UI. |
| src/CodeShellManager/ViewModels/SessionViewModel.cs | Adds RepoRoot/sibling flags, shared accent keying, and worktree subtitle. |
| src/CodeShellManager/ViewModels/MainViewModel.cs | Adds group filter state, selection state, and group/session membership operations. |
| src/CodeShellManager/Services/SessionManager.cs | Adds group CRUD/move APIs, session insert-after-parent, and legacy group migration. |
| src/CodeShellManager/Services/GitService.cs | Adds repo identity resolution, worktree parsing, branch listing, and worktree add helper. |
| src/CodeShellManager/Models/AppState.cs | Introduces GroupDisplayMode and new settings flags; drops default “Default” group. |
| src/CodeShellManager/MainWindow.xaml.cs | Implements group strip + inline headers, multi-select, worktree clustering, and new context actions. |
| src/CodeShellManager/MainWindow.xaml | Adds group strip column and fixes sidebar header “+” layout. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| newIndex = Math.Clamp(newIndex, 0, _groups.Count - 1); | ||
| if (cur == newIndex) return; | ||
| _groups.RemoveAt(cur); |
| { | ||
| var visibleIds = SidebarSessionList.Children.OfType<Border>() | ||
| .Select(b => b.Tag as string) | ||
| .Where(t => t != null && !t.StartsWith("dormant:")) |
| if (Directory.Exists(TargetPath) && Directory.EnumerateFileSystemEntries(TargetPath).GetEnumerator().MoveNext()) | ||
| { | ||
| MessageBox.Show(this, | ||
| $"'{TargetPath}' already exists and is non-empty. git worktree add will refuse to use it.", | ||
| "Folder not empty", MessageBoxButton.OK, MessageBoxImage.Warning); | ||
| return; |
| GitInfoLoaded = true; | ||
|
|
||
| // RepoRoot is stable for the life of the session — resolve it once. | ||
| if (RepoRoot == null && !string.IsNullOrEmpty(branch)) |
Code reviewFound 1 issue:
CodeShellManager/src/CodeShellManager/MainWindow.xaml.cs Lines 2098 to 2125 in d2c9b71 Fix: only Also worth a look (lower confidence, not posted as separate issues)
Installer audit (separate from review): no new files in 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Bundles 9 small fixes raised in PR #28 review: - ResolveInlineSessionDropTarget no longer emits empty leading/trailing sections, so a drop above the first group header lands at the top of that group instead of silently retargeting to the end of Ungrouped. - MoveGroup compensates for RemoveAt when moving downward and accepts Count as a legal "insert at end" target, fixing the off-by-one reorder. - Shift-click range selection excludes "groupheader:" and "cluster:" tags, so SelectedSessionIds can no longer contain header IDs. - NewWorktreeDialog directory check uses Any() and a try/catch — an inaccessible target path now produces a friendly warning instead of crashing the dialog. - SessionViewModel resolves RepoRoot regardless of branch presence, so detached-HEAD worktrees participate in sibling detection, shared accent color, and clustering. - LaunchAndFollowUpWorktreesAsync staggers consecutive claude launches with the same 2s delay the boot path uses (commit 59a7067) to avoid re-introducing ~/.claude.json corruption when adding several sibling claude sessions at once. - _selectionAnchorId is cleared when its session is closed or slept, so the next shift-click can't collapse to a single item. - The legacy auto-"Default" group migration is now gated on a one-shot AppSettings.LegacyDefaultGroupCleared flag, so a user-named "Default" group created after the upgrade can't be wiped on restart. - _ungroupedExpanded promoted to AppSettings.UngroupedSectionExpanded so the collapse state of the implicit Ungrouped header survives restarts, matching the behavior of real SessionGroup.IsExpanded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Three related sidebar features for taming a large session list:
All/Ungrouped/ per-group), with notification pips per tab (alert count / approval / input).▶/▼ GroupName Nrows directly in the sidebar; all groups visible at once, expand/collapse per group (persisted viaSessionGroup.IsExpanded). Ungrouped sessions sit under an implicit header at the top.Add to group ▸/Remove from group/Sleep/Close. Drag a session onto a group tab/header to assign it. Drag group tabs/headers to reorder (right-click also offersMove up/Move down). Worktree clusters still form within each section.New worktree from this branch…creates a sibling worktree (new or existing branch) and launches a session cloning the source's command, args, group, and profile. The New Session dialog auto-detects existing sibling worktrees and offers a multi-select "Also start sessions in:" panel. A right-click → "New session in sibling worktree ▸" submenu adds an already-existing worktree on demand. Sibling identity usesgit rev-parse --git-common-dir(shared across all worktrees of a repo) so siblings always match, share an accent color, and cluster automatically under a small📁 repo · Nheader.Ctrl+Shift+Tduplicates the active session (browser-style). New sessions land atparentIndex+1in bothSessionManager.Sessionsand the VMSessionscollection, inheriting the parent's group + profile. Top-level "+ New Session" while a group filter is active also lands the new session in that group.Also fixes the sidebar header
+button being right-clipped and vertically misaligned withSESSIONS, and gives the dark theme proper coverage on the worktree dialog's branch dropdown.Related issues
git worktree list --porcelainparsing, auto-detect siblings in the New Session dialog with opt-in multi-add, on-demand "sibling worktree" submenu, shared accent color across worktree siblings, automatic cluster wrapper with📁 repo · Nheader, and a dedicatedgit worktree addflow.Ctrl+Shift+T, dialog pre-filled with parent's folder/command, GroupId + profile inheritance, and placement atparentIndex+1.Migration
SessionManager.LoadFromStatedetects the lone legacy group (count == 1, name == "Default", SortOrder == 0) and silently drops it, clearing the matchingGroupIds. Result: existing users see no group tabs/headers at all until they create one themselves.ShowGroupsTab→GroupDisplayMode. An earlier iteration of this branch added aShowGroupsTabboolean; that's been superseded by the three-wayGroupDisplayModeenum. On load, ifGroupDisplayModeis still at its default (FilterStrip) AND the legacyShowGroupsTabisfalse, the mode is migrated toNone.ShowGroupsTabis kept in sync on save (= mode != None) so a downgrade to an older build still respects the "no strip" choice.Test plan
Recommend running with
--cleanfor an isolated state during exploration:Sidebar polish
+fix: narrow the sidebar; the+no longer clips on the right and is vertically aligned with theSESSIONSlabel.Group display modes (#24)
All/Ungrouped/ per-group tabs. Auto-hidden when no groups exist.▶/▼ GroupName Nrow in the sidebar; click toggles expand/collapse (persisted across restarts). Ungrouped sessions sit under an implicit "Ungrouped" header. The filter strip is hidden in this mode.Add to group ▸ New group…. Sessions move under the new group.Worktrees (#23)
New worktree from this branch…. Pick "Create new branch from current" or "Use existing branch", confirm. New session launches in the worktree folder.📁 repo ⎇ branchsubtitle, and (with the cluster setting on) sit under a📁 repo · 2header.Spawn near parent (#27)
Duplicate session, or pressCtrl+Shift+Twith the session active. Same folder, same command, derived name, lands after parent.Migration
Defaultgroup disappears silently; sessions are not lost.ShowGroupsTab=falsesetting saved should come up inNonemode (no strip, no inline headers).Out of scope / not in this PR
🤖 Generated with Claude Code