Skip to content

Scope grid + remember layout per group (closes #30)#35

Merged
AThraen merged 4 commits into
mainfrom
feat/per-group-layout-filter
May 13, 2026
Merged

Scope grid + remember layout per group (closes #30)#35
AThraen merged 4 commits into
mainfrom
feat/per-group-layout-filter

Conversation

@mortenaslo
Copy link
Copy Markdown
Contributor

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):

  • Limit terminal grid to active group (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.
  • Remember layout per group (AppSettings.PerGroupLayout + AppState.GroupLayouts) — each group remembers its own LayoutMode (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.EffectiveActiveGroupId resolves the "current group" per display mode:

Mode Source
FilterStrip the explicitly selected tab (ActiveGroupId)
InlineHeaders the active session's GroupId (no tab strip exists, so the focused session is the implicit selector)
None null — no group concept, no filter

OnActiveGroupIdChanged + OnActiveSessionChanged route to a common HandleEffectiveGroupChanged() that does the save-old / restore-new dance, then flushes SaveStateAsync (because the group-strip click handler doesn't itself persist).

Storage

AppState.GroupLayouts: Dictionary<string, string> keyed by group id, GroupFilter.Ungrouped, or GroupFilter.AllKey ("__ALL__") for the no-filter view. Missing keys fall back to LastLayout. 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

  • FilterStrip mode: click group A → grid shows only A's sessions; switch to B → grid shows only B; toggle setting off → grid shows all.
  • InlineHeaders mode: click a session in group A → grid filters to A; click one in B → grid filters to B.
  • PerGroupLayout: set 2×2 in group A, switch to B, set 4-col; round-trip to A — A is 2×2, B is 4-col.
  • Restart app — saved per-group layouts survive.
  • Toggle PerGroupLayout off — switching groups stops changing the layout.
  • None mode — no filter, no layout swap.

Closes #30.

🤖 Generated with Claude Code

mortenaslo and others added 2 commits May 13, 2026 15:12
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>
Stagger Claude restart + show restore loading indicators
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@AThraen AThraen merged commit 13bf935 into main May 13, 2026
1 check passed
@AThraen AThraen deleted the feat/per-group-layout-filter branch May 13, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Groups own pane/grid layout (idea)

3 participants