Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,16 @@ Confirmation is driven by the `confirmHeadline` parameter rather than a count te

`RestartSessionsAsync` runs its targets **strictly sequentially** — `RestartSessionAsync` waits for each Claude process to exit before starting its replacement, and awaiting those in parallel would put several `claude.exe` instances back on the shared config file at once, which is the race the wait exists to prevent. A `_restartInProgress` flag drops (rather than queues) an overlapping invocation for the same reason — see the guard note above. That sequencing is also why the bulk entry points confirm: restarting a fleet of Claude sessions is a minute-scale operation, not an instant one. Each target is re-resolved per iteration, since every restart replaces the `SessionViewModel` for that id and an earlier one in the loop may have failed into dormant.

**The rail, and why it is not the shutdown board.** A bulk restart drives `RestartRail` (2px, docked under `RestoreRail`) plus a `RestartPill` counter in the toolbar's right stack, via `SetRestartProgress`. That is the *restore* pattern, deliberately, and the choice is forced: `ShutdownOverlay` only works because `OnClosing` collapses `TerminalGrid` before showing it — WebView2 is an `HwndHost` and composites over any WPF overlay — and collapsing the grid here would black out every live pane for the whole run. A bulk restart does not block the user, so it gets the quiet rail; shutdown does, so it gets the board. Both are hidden for a single target: the toolbar ↻ restarts in seconds and a rail that appears and vanishes is noise.

The restart pair is **separate from the restore pair rather than a shared rail** — two unrelated loops with unrelated lifetimes, and one shared rail would need a precedence rule to no benefit. Peach (`#fab387`, the shutdown board's "closing…" colour) rather than the restore blue.

**A restart refuses to start while the restore loop is running** (`_restoreInProgress`, dropped with a toast like `_restartInProgress`). Both loops spawn `claude.exe` and neither one's stagger can see the other's launches, so overlapping them is exactly the unlocked read-modify-write on `~/.claude.json` that `ClaudeLaunchStaggerMs` exists to prevent. The `OnLoaded` restore loop awaits per session and leaves the quick menu clickable throughout, so this is an ordinary click on a 55-session start, not a corner case. An earlier version of this section cited that overlap as the *reason* for separate rails — writing a config race down as a layout question. Both single-session `RestartSessionAsync` and bulk `RestartSessionsAsync` carry the guard; one session is enough to lose the race.

**Stop means "after the current one".** The `⏹` on the pill sets `_restartCancelRequested`, read at the **top** of each loop iteration. It cannot abandon the session already mid-restart: by that point the VM is out of `_vm.Sessions` and the PTY is down, so returning early would strand it as dormant — the failure path, not a stop. Targets not yet reached keep their live terminals untouched, and a toast reports how many were skipped. The flag clears in the same `finally` as `_restartInProgress`.

**The confirmation quotes a duration**, built by the WPF-free `Services/BulkRestartEstimate` (tested in `BulkRestartEstimateTests`) so the wording is unit-testable at all. It costs a Claude target at 4–8s (exit wait 2.3–4.7s + stagger + ~1.4s relaunch) and a plain shell at 1–2s, reports a range rather than false precision, and switches to minutes only once the *optimistic* bound passes 60s. At or above `ScaleWarningThreshold` (10 targets) it also says the restarts are sequential and names how many are Claude — that count is what makes the estimate look earned rather than arbitrary. `MessageBox` cannot emphasise anything, so scale is carried by the words; making the warning *look* like one would mean a themed window instead.

## Editing a Session's Configuration

Any existing session can be reconfigured through the **same form used to create one** —
Expand Down
56 changes: 56 additions & 0 deletions src/CodeShellManager/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,46 @@
</StackPanel>

<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<!-- Bulk-restart counter — paired with RestartRail, and a SEPARATE pair
from the restore one below rather than a shared rail: the two are
driven by unrelated loops with unrelated lifetimes, and one shared
rail would need a precedence rule for no gain.

Note the two rails are NOT expected on screen together: a restart
refuses to start while the restore loop is running, because both
spawn claude.exe and neither stagger can see the other's launches
(see _restoreInProgress). An earlier version of this comment cited
the overlap as the reason for separate pairs, which quietly wrote
down a config-writer race as a layout question.

Peach rather than the restore blue because peach is already the
shutdown board's "closing…" colour — it reads as teardown in
progress. -->
<Border x:Name="RestartPill" Background="#181825" CornerRadius="10"
BorderBrush="#313244" BorderThickness="1"
Margin="0,0,8,0" VerticalAlignment="Center"
Visibility="Collapsed">
<DockPanel>
<!-- ToolBtn, not the default Button template. This is the only
button in the app that is ever disabled, and Aero2's stock
template has an IsEnabled=false trigger that sets its
Background/BorderBrush/Foreground with TargetName — which
outranks both the Transparent TemplateBinding and the muted
Foreground set in SetRestartProgress. The ⏹ would render as
a light-grey #F4F4F4 box inside the dark pill. ToolBtn's
template has no disabled trigger, so the code-set brush
actually applies. -->
<Button x:Name="RestartStopBtn" DockPanel.Dock="Right" Content="⏹"
Style="{StaticResource ToolBtn}"
Foreground="#fab387" FontSize="10" Padding="4,2,8,2"
ToolTip="Stop once the session currently restarting finishes"
Click="RestartStop_Click"/>
<TextBlock x:Name="RestartPillText" Text="restarting…" Foreground="#fab387"
FontSize="11" FontWeight="SemiBold" Padding="9,2,4,2"
VerticalAlignment="Center"/>
</DockPanel>
</Border>

<!-- Restore counter — paired with RestoreRail; both live only for the
duration of the restore loop in OnLoaded. -->
<Border x:Name="RestorePill" Background="#181825" CornerRadius="10"
Expand Down Expand Up @@ -197,6 +237,22 @@
Style="{StaticResource FlatBar}" Background="Transparent"
Minimum="0" Maximum="1" Value="0" Visibility="Collapsed"/>

<!-- ── Bulk-restart rail ────────────────────────────────────────────
Same reasoning as the restore rail above — an HwndHost composites over any
overlay, so the toolbar strip is where an indicator is guaranteed to show.
A restart queue is strictly sequential and each Claude session waits for its
old process to exit, so "Restart all" on a large fleet runs for minutes; this
is the only thing on screen that says so while it does.

NOT the shutdown board: that only works because OnClosing collapses
TerminalGrid first, and doing that here would black out every live pane for
the whole run. A bulk restart does not block the user; the restore rail is
the right precedent. -->
<ProgressBar x:Name="RestartRail" DockPanel.Dock="Top" Height="2"
Style="{StaticResource FlatBar}" Background="Transparent"
Foreground="#fab387"
Minimum="0" Maximum="1" Value="0" Visibility="Collapsed"/>

<!-- ── Command helper panel ─────────────────────────────────────── -->
<Border x:Name="CommandHelperPanel" DockPanel.Dock="Top" Background="#11111b"
BorderThickness="0,0,0,1" BorderBrush="#313244"
Expand Down
Loading