Stagger Claude restart + show restore loading indicators#37
Merged
AThraen merged 1 commit intoMay 13, 2026
Merged
Conversation
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>
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent Claude session collisions that can corrupt the user’s ~/.claude.json by introducing configurable staggering between Claude launches and by serializing Claude shutdown behavior, while also improving perceived responsiveness during restore by showing “Launching…” sidebar placeholders.
Changes:
- Add
AppSettings.ClaudeLaunchStaggerMs(default 2000) and wire it into Settings UI + restore-time launch pacing. - Add restore-time sidebar placeholders for launching sessions and weave them into sidebar ordering/filtering logic.
- Update app shutdown to dispose non-Claude sessions in parallel while handling Claude sessions sequentially with an exit wait + post-exit pause.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/CodeShellManager/Models/AppState.cs | Adds the new ClaudeLaunchStaggerMs persisted setting. |
| src/CodeShellManager/Views/SettingsWindow.xaml | Adds UI field + help text for configuring the Claude stagger. |
| src/CodeShellManager/Views/SettingsWindow.xaml.cs | Wires the new stagger setting into clone/load/save logic. |
| src/CodeShellManager/MainWindow.xaml.cs | Uses the stagger for restore/worktree launch pacing; adds launching placeholders; updates sidebar rebuild logic; updates shutdown disposal flow for Claude sessions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
4252
to
+4282
| protected override async void OnClosing(System.ComponentModel.CancelEventArgs e) | ||
| { | ||
| _windowStateTimer.Stop(); | ||
| if (_windowStateReady) | ||
| _vm.UpdateWindowState(WindowState, Left, Top, Width, Height); | ||
| await _vm.SaveStateAsync(); | ||
| foreach (var vm in _vm.Sessions.ToList()) | ||
| vm.Dispose(); | ||
|
|
||
| var all = _vm.Sessions.ToList(); | ||
|
|
||
| // Non-Claude sessions don't fight over ~/.claude.json — dispose them in parallel. | ||
| foreach (var vm in all) | ||
| { | ||
| if (!ClaudeSessionService.IsClaudeCommand(vm.Command)) | ||
| vm.Dispose(); | ||
| } | ||
|
|
||
| // Claude rewrites ~/.claude.json on exit without locking, so two claude.exe | ||
| // processes flushing simultaneously can corrupt it. Dispose claude sessions one | ||
| // at a time, waiting for each process to *actually exit* before starting the next | ||
| // — a fixed time stagger isn't safe because claude's shutdown can take longer | ||
| // than the configured delay on slow disks. Cap each wait at 10s so a stuck claude | ||
| // doesn't hang application shutdown. | ||
| int postExitMs = _vm.Settings.ClaudeLaunchStaggerMs; | ||
| foreach (var vm in all) | ||
| { | ||
| if (!ClaudeSessionService.IsClaudeCommand(vm.Command)) continue; | ||
| await DisposeAndWaitForExitAsync(vm, timeoutMs: 10000); | ||
| // Small post-exit pause as belt-and-braces in case ~/.claude.json's write | ||
| // continues after the parent's shutdown signal but before its handles close. | ||
| if (postExitMs > 0) await Task.Delay(Math.Min(postExitMs, 1000)); | ||
| } |
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
145
to
+149
| _edited.DefaultWorkingFolder = DefaultFolderBox.Text.Trim(); | ||
| _edited.AutoRestoreSessions = AutoRestoreCheck.IsChecked == true; | ||
| _edited.AutoResumeClaude = AutoResumeClaudeCheck.IsChecked == true; | ||
| if (int.TryParse(ClaudeLaunchStaggerBox.Text, out int staggerMs) && staggerMs >= 0) | ||
| _edited.ClaudeLaunchStaggerMs = staggerMs; |
| <StackPanel Margin="0,10,0,0"> | ||
| <TextBlock Text="Claude launch / shutdown stagger (ms)" Style="{StaticResource Label}"/> | ||
| <TextBox x:Name="ClaudeLaunchStaggerBox" Width="100" HorizontalAlignment="Left"/> | ||
| <TextBlock Text="Delay between consecutive Claude sessions during restore-on-startup and app shutdown. Claude's CLI rewrites ~/.claude.json on startup/exit without locking, so back-to-back Claude processes can corrupt it. 2000 ms is the safe default; set 0 to disable." |
AThraen
added a commit
that referenced
this pull request
May 13, 2026
Conflicts: OnLoaded restore loop and OnClosing teardown both diverged from the staggered-shutdown work that landed in #37. Resolutions: - OnLoaded: keep main's launching-placeholder + sequential staggered launch flow; layer this PR's batched WebView2-access-denied dialog on top so a single consolidated message replaces the previous N per-session popups. Drop the duplicate dormant-loop tail (main now adds dormant entries up-front before launches start). - OnClosing: keep main's claude-aware sequential disposal and DisposeAndWaitForExitAsync wait; layer this PR's try/catch around _db.Close()/_db.Dispose() in to swallow the SqliteConnection NRE observed during shutdown. Review fixes (from PR self-review): - OnClosing was async void with multiple awaits, which WPF doesn't wait for. Switched to the standard e.Cancel=true + reclose pattern gated by _shutdownComplete so async cleanup actually finishes before the window tears down. - _lastOutputTickMs in TerminalBridge.OnPtyData was read-modify-written without a memory barrier. Replaced with Interlocked.Exchange — atomic read+write in one call, and gives the JIT a barrier even though the PTY read loop is currently single-threaded.
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.
Summary
Fixes the
~/.claude.jsoncorruption error users hit on app close (and occasionally on restore) plus makes restore-on-startup feel responsive. Three changes that work together:AppSettings.ClaudeLaunchStaggerMs(default 2000) — one setting drives the wait between consecutive Claude session starts and shutdowns. The prior hardcodedTask.Delay(2000)only covered the restore path; close-time had no pacing, so twoclaude.exeprocesses could rewrite~/.claude.jsonsimultaneously and corrupt it. Exposed in Settings → Session Settings with help text and a 0-disables footnote.OnClosing— non-Claude sessions still dispose in parallel. For each Claude session,DisposeAndWaitForExitAsyncsubscribes to the PTY'sExitedevent, callsvm.Dispose()(which triggersClosePseudoConsole→ child gets the shutdown signal), 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 real exit beats a fixed timer because Claude's shutdown can take longer than the configured stagger on slow disks.OnLoadednow stages a muted placeholder sidebar row for every live session up-front, each with a pulsing accent dot and a Launching… subtitle.RebuildSidebarOrderwalksSessionManager.Sessionsin saved order and resolves each entry to either the live sidebar item (if registered) or the placeholder, so the full list of icons appears the moment the window opens and each row lights up in place as its session finishes booting. Placeholders are dropped automatically once the real entry is registered (or the launch fails).Why now
User report: app close was producing a JSON parse error in
~/.claude.jsonwith multiple Claude sessions open. Root cause is Claude's unlocked read-modify-write on that file at startup and on graceful exit — the restore-side mitigation already existed (commit 59a7067) but the shutdown side did not.Files touched
src/CodeShellManager/Models/AppState.cs— new settingsrc/CodeShellManager/Views/SettingsWindow.xaml+.xaml.cs— UI field + clone/load/save wiringsrc/CodeShellManager/MainWindow.xaml.cs— uses the setting in both stagger spots, sequential close, placeholder infra (_launchingSidebarItems,BuildLaunchingSidebarItem,ResolveinRebuildSidebarOrder,AppendSessionsWithClusterssignature updated to carry placeholder items)Base
Targeted at
feat/per-group-layout-filterbecause that branch is still open in #35 and these changes are stacked on top of it. Will rebase tomainonce #35 lands.Test plan
~/.claude.jsonmay corrupt, which is the documented trade-off)~/.claude.jsonstays valid JSON (parses cleanly) after each subsequent runclaude.exeexternal to the app, then close — shutdown still finishes (10 s timeout fallback)--cleanmode: still skips restore (no placeholders shown), still skips state save on close🤖 Generated with Claude Code