Scope grid + remember layout per group (closes #30)#35
Merged
Conversation
Adds two settings (both default on) so groups can own the visible terminal panes and their layout, addressing #30: - FilterGridByActiveGroup: when a group is the effective filter, the multi-pane grid only shows that group's sessions. In FilterStrip mode the filter is the selected tab; in InlineHeaders mode it's derived from the active session's group (no tab strip exists there). - PerGroupLayout + AppState.GroupLayouts: each group remembers its own Layout (Single / 2x2 / etc.). Crossing a group boundary saves the old group's layout (seeded if absent) and restores the new group's. Sessions outside the active group stay live — PTY, indexing, and alerts keep running; they're just not rendered until the filter swings back. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three lifecycle improvements aimed at preventing ~/.claude.json corruption and making session restore feel responsive. - AppSettings.ClaudeLaunchStaggerMs (default 2000): a single setting drives the wait between consecutive Claude session starts and shutdowns. The prior hardcoded 2 s only covered restore-on-startup; close-time had no pacing at all, so two claude.exe processes could rewrite ~/.claude.json concurrently and corrupt it. - Claude-aware OnClosing: non-Claude sessions still dispose in parallel. For each Claude session the new DisposeAndWaitForExitAsync subscribes to the PTY's Exited event, calls Dispose (which triggers ClosePseudoConsole and signals the child), then awaits actual process exit (capped at 10 s so a stuck child can't hang shutdown). A small post-exit pause acts as belt-and-braces. Waiting on the real exit beats a fixed timer because claude's shutdown can take longer than the configured stagger on slow disks. - Restore-time loading indicators: OnLoaded now stages a muted placeholder sidebar row for every live session up-front, each with a pulsing accent dot and a "Launching…" subtitle. RebuildSidebarOrder walks SessionManager.Sessions in saved order, picking the live sidebar item if registered else the placeholder (Resolve helper), so the full list of icons appears the moment the window opens and items light up in place as each session finishes booting. Placeholders are dropped automatically when the real entry is registered (or the launch fails). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7 tasks
Stagger Claude restart + show restore loading indicators
There was a problem hiding this comment.
Pull request overview
This PR implements issue #30 by making session groups optionally “own” the terminal grid’s visible session set and (optionally) the grid layout, so switching groups can swap the entire terminal view/layout instead of only changing the sidebar grouping.
Changes:
- Adds two Appearance settings: Limit terminal grid to active group and Remember layout per group, plus persisted
AppState.GroupLayouts. - Updates terminal rendering and sidebar rebuild logic to support group-scoped grids and “launching…” sidebar placeholders during restore-on-startup.
- Adds a configurable Claude launch/shutdown stagger setting and uses it to stagger sequential Claude restores (and attempts to serialize Claude shutdown on app close).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/CodeShellManager/Views/SettingsWindow.xaml.cs |
Wires new settings into the settings dialog (clone, load, save). |
src/CodeShellManager/Views/SettingsWindow.xaml |
Adds UI for Claude stagger + group/grid behavior checkboxes. |
src/CodeShellManager/ViewModels/MainViewModel.cs |
Implements effective group keying + per-group layout persistence/restore logic. |
src/CodeShellManager/Models/AppState.cs |
Adds new settings and persistent GroupLayouts dictionary to state. |
src/CodeShellManager/MainWindow.xaml.cs |
Filters terminal panes by effective group, adds restore placeholders, updates sidebar clustering, and adds Claude stagger/shutdown sequencing logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+2655
to
+2663
| (Border item, SessionViewModel? vm)? Resolve(ShellSession s) | ||
| { | ||
| if (s.IsDormant) return null; | ||
| var liveVm = _vm.Sessions.FirstOrDefault(v => v.Id == s.Id); | ||
| if (liveVm != null && _sessionUi.TryGetValue(liveVm.Id, out var ui)) | ||
| return (ui.sidebarItem, liveVm); | ||
| if (_launchingSidebarItems.TryGetValue(s.Id, out var ph)) | ||
| return (ph, null); | ||
| return null; |
Comment on lines
+4291
to
+4318
| /// Signals the session's PTY to shut down and waits for its child process to actually | ||
| /// exit (or <paramref name="timeoutMs"/> ms, whichever comes first), then fully | ||
| /// disposes the VM. Used for claude sessions on app close so consecutive | ||
| /// <c>~/.claude.json</c> writes can't overlap. | ||
| /// </summary> | ||
| private static async Task DisposeAndWaitForExitAsync(SessionViewModel vm, int timeoutMs) | ||
| { | ||
| var pty = vm.Pty; | ||
| if (pty == null || !pty.IsRunning) | ||
| { | ||
| vm.Dispose(); | ||
| return; | ||
| } | ||
|
|
||
| var tcs = new TaskCompletionSource(); | ||
| void OnExit() => tcs.TrySetResult(); | ||
| pty.Exited += OnExit; | ||
| try | ||
| { | ||
| // Dispose triggers ClosePseudoConsole, which signals the child to shut down. | ||
| // MonitorExitAsync (already running) will fire Exited once the process exits. | ||
| vm.Dispose(); | ||
| await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs)); | ||
| } | ||
| finally | ||
| { | ||
| pty.Exited -= OnExit; | ||
| } |
Resolve(ShellSession) was doing _vm.Sessions.FirstOrDefault(v => v.Id == s.Id) per saved session. Snapshot _vm.Sessions into a dictionary at the top of the method so each Resolve call is O(1). RebuildSidebarOrder fires on many UI events (filter change, membership change, drag-reorder, launch) so the saved- list × live-list scan was the right thing to flatten. Addresses Copilot review feedback on PR #35. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the idea in #30: groups now own the visible pane set and the grid layout, so switching groups swaps the entire terminal view rather than just decorating the sidebar.
Summary
Two new settings under Appearance (both default on):
AppSettings.FilterGridByActiveGroup) — when a group is the effective filter, the multi-pane grid only shows that group's sessions. Sessions outside the filter stay alive (PTY, indexing, alerts) — they just don't render until the filter swings back.AppSettings.PerGroupLayout+AppState.GroupLayouts) — each group remembers its ownLayoutMode(Single / 2×2 / etc.). Crossing a group boundary seeds the old group's slot (if empty) and restores the new group's saved layout.Effective filter
MainViewModel.EffectiveActiveGroupIdresolves the "current group" per display mode:ActiveGroupId)GroupId(no tab strip exists, so the focused session is the implicit selector)OnActiveGroupIdChanged+OnActiveSessionChangedroute to a commonHandleEffectiveGroupChanged()that does the save-old / restore-new dance, then flushesSaveStateAsync(because the group-strip click handler doesn't itself persist).Storage
AppState.GroupLayouts: Dictionary<string, string>keyed by group id,GroupFilter.Ungrouped, orGroupFilter.AllKey("__ALL__") for the no-filter view. Missing keys fall back toLastLayout. Initial load is guarded so the LastLayout assignment doesn't clobber the freshly-loaded dict.Single-pane fallback
When the filter hides the active session, the Single-mode pane falls back to the first visible session instead of rendering a hidden tab.
Test plan
Closes #30.
🤖 Generated with Claude Code