From e3868639e380e103a14ca90e6f9737dae6ea437f Mon Sep 17 00:00:00 2001 From: Allan Thraen Date: Sat, 16 May 2026 11:44:39 +0200 Subject: [PATCH 01/16] =?UTF-8?q?feat(bundle):=20low-hanging=20fruit=20?= =?UTF-8?q?=E2=80=94=20#51=20#53=20#54=20#55=20#39=20#46?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six low-effort / high-impact changes bundled on one branch: #53 Run-command output: strip CSI private-mode (e.g. ESC[?9001h) and OSC ST. The shared regex in RunInstance + OutputIndexer now accepts `?` in CSI params and recognises both BEL and ESC\ as OSC terminators. #54 RunCommandItem.Mode — Process (default, cmd /c) vs PowerShell (pwsh -EncodedCommand UTF-16LE base64; falls back to powershell.exe). SSH runs ignore Mode — remote always goes through bash. #55 RunCommandItem.PostRunUrl — opens URL via ShellExecute on exit code 0. Editor dialog grows Mode + URL columns; dialog widened 720→900. #51 LayoutMode.ThreeByThree — new 3×3 (9-pane) grid layout and toolbar btn. #39 PseudoTerminal MonitorExitAsync now waits on a DuplicateHandle so the Dispose path can close _hProcess without racing the wait. Eliminates the Win32 UB that could fire Exited before the child actually exited. #46 Recently-closed ring buffer (cap 10, persisted to state.json): - new Models/RecentlyClosedEntry.cs - Ctrl+Shift+T pops & reopens the newest entry (browser convention) - duplicate-session moves to Ctrl+Alt+T - "Recently closed" list at the top of the New Session dialog - FromSession deep-copies RunCommands with fresh Ids - --clean mode skips push, matching SaveStateAsync semantics Verification • dotnet build: 0 errors, 30 pre-existing warnings • dotnet test: 90/90 pass (was 82; +8 new tests covering BuildPwshArgs, RecentlyClosedEntry.FromSession, Subtitle, RunCommandItem defaults) CLAUDE.md updated: new keybindings, Recently Closed section, RunCommandItem data model with Mode + PostRunUrl semantics. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 20 ++- src/CodeShellManager/MainWindow.xaml | 2 + src/CodeShellManager/MainWindow.xaml.cs | 78 ++++++++++- src/CodeShellManager/Models/AppState.cs | 6 + .../Models/RecentlyClosedEntry.cs | 91 ++++++++++++ src/CodeShellManager/Models/RunCommandItem.cs | 22 +++ src/CodeShellManager/Services/RunInstance.cs | 76 +++++++++- .../Terminal/OutputIndexer.cs | 6 +- .../Terminal/PseudoTerminal.cs | 32 ++++- .../ViewModels/MainViewModel.cs | 42 +++++- .../Views/NewSessionDialog.xaml | 15 +- .../Views/NewSessionDialog.xaml.cs | 65 ++++++++- .../Views/SessionRunCommandsDialog.xaml | 39 ++++-- .../Views/SessionRunCommandsDialog.xaml.cs | 18 +++ .../RecentlyClosedEntryTests.cs | 132 ++++++++++++++++++ .../RunCommandItemTests.cs | 10 ++ .../RunInstanceTests.cs | 15 ++ 17 files changed, 650 insertions(+), 19 deletions(-) create mode 100644 src/CodeShellManager/Models/RecentlyClosedEntry.cs create mode 100644 tests/CodeShellManager.Tests/RecentlyClosedEntryTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index f4b6f42..ed20edc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,6 +155,19 @@ When any override is set, `LaunchSessionAsync` calls `bridge.ApplyProfileOverrid **Once stamped, profile overrides are independent.** A session keeps its appearance even if the user later edits or deletes the source profile in Windows Terminal. +## Recently Closed Sessions + +Closing a session (`Ctrl+W`, sidebar `✕`, or terminal-toolbar close) pushes a snapshot onto a ring buffer (`AppState.RecentlyClosed`, cap `MainViewModel.MaxRecentlyClosed = 10`, newest first). Two ways to reopen: + +- **`Ctrl+Shift+T`** — pops the newest entry and re-launches it via `MainWindow.ReopenClosedSessionAsync`. The reopened session gets a **fresh Id** so it's independent of anything that may still reference the old one. +- **"Recently closed" list at the top of the New Session dialog** — click an entry to reopen it; that entry is removed from the ring. + +Sleep/wake doesn't touch the ring (`SleepSession` bypasses `OnSessionCloseRequested`). `--clean` mode neither pushes new entries nor persists the existing ring, matching `SaveStateAsync` semantics. + +The snapshot model is `Models/RecentlyClosedEntry.cs` — a separate POCO from `ShellSession` so PTY/runtime fields (`IsDormant`, `Status`, `LastActivityAt`) don't leak into the ring buffer. `RunCommands` are deep-copied with fresh Ids on both snapshot creation and session recreation, so edits to either side never alias the other. + +FTS5 scrollback retention is **out of scope** for v1 — restored sessions start with an empty xterm buffer. + ## Sleep / Wake (Dormant Sessions) Sessions can be put to sleep instead of closed — the PTY is torn down but the `ShellSession` is kept in `state.json` (`IsDormant = true`) so it can be relaunched from the sidebar later. Useful when you have many long-running projects but only need a few live at once. @@ -175,7 +188,10 @@ Sessions can be put to sleep instead of closed — the PTY is torn down but the Each session can have a list of "run commands" — labelled command lines invoked by the toolbar ▶ button, the F5 keybinding, or the sidebar right-click submenu. Runs spawn a **separate headless `PseudoTerminal`** in the session's working folder (or a fresh `ssh` connection for SSH parents); they do **not** type into the parent PTY, so a Claude session is untouched. -**Data:** `ShellSession.RunCommands: List { Id, Label, CommandLine, IsDefault }`. Exactly one item has `IsDefault=true`; see `RunCommandItem.EnsureSingleDefault`. Persisted to `state.json`. +**Data:** `ShellSession.RunCommands: List { Id, Label, CommandLine, IsDefault, Mode, PostRunUrl }`. Exactly one item has `IsDefault=true`; see `RunCommandItem.EnsureSingleDefault`. Persisted to `state.json`. + +- **`Mode`** (`RunMode.Process` default / `RunMode.PowerShell`) — `Process` runs through `cmd /c` as before; `PowerShell` wraps the command line in `pwsh.exe -NonInteractive -NoLogo -ExecutionPolicy Bypass -EncodedCommand ` (falls back to `powershell.exe` if `pwsh` isn't on PATH). SSH parents ignore `Mode` — remote runs always go through bash. Use PowerShell when the command relies on pipes (`|`), redirection (`>`), `$env:` variables, or cmdlets. +- **`PostRunUrl`** (`string?`, default `null`) — when set and the run exits with code 0, `Process.Start` opens the URL via `UseShellExecute=true` (default browser). Failures are swallowed; no health-check polling. **Templates:** `RunCommandTemplatesService.SeedFor(folder)` detects project type (top-level scan, first-match: dotnet → cargo → node → python → make) and returns a seed list with fresh Ids. Templates are *copied* onto new sessions at creation time; subsequent edits don't propagate back. SSH sessions skip detection (empty list). @@ -231,6 +247,8 @@ Persisted in `state.json`. Key settings: | Key | Action | |---|---| | `Ctrl+T` | New session | +| `Ctrl+Shift+T` | Reopen the most-recently-closed session (browser convention) | +| `Ctrl+Alt+T` | Duplicate active session (was `Ctrl+Shift+T` pre-bundle) | | `Ctrl+W` | Close active session | | `Ctrl+F` | Toggle search | | `Ctrl+Tab` | Cycle sessions | diff --git a/src/CodeShellManager/MainWindow.xaml b/src/CodeShellManager/MainWindow.xaml index 7c3ee6b..bd800f6 100644 --- a/src/CodeShellManager/MainWindow.xaml +++ b/src/CodeShellManager/MainWindow.xaml @@ -139,6 +139,8 @@ Click="Layout_SixTwo_Click" FontSize="9" Padding="4,4"/>