diff --git a/CLAUDE.md b/CLAUDE.md index f4b6f42..fab37e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ dotnet run --project src/CodeShellManager/CodeShellManager.csproj | `--clean` | Debug isolation mode — see below. | **`--clean`** (parsed in `App.OnStartup`, exposed as `App.CleanStart`): -- `MainWindow.OnLoaded` skips the restore loop and clears the in-memory `SessionManager` so any new sessions in the run don't co-mingle with the persisted set. +- `MainWindow.OnLoaded` skips the restore loop and clears the in-memory `SessionManager` — both sessions AND groups — so any new work in the run starts from a blank slate. - `MainViewModel.SaveStateAsync` short-circuits — **nothing is written to `state.json`** for the entire run. Window bounds, layout changes, settings tweaks, and any sessions created during the clean run are all discarded on exit. - The user's prior `state.json` survives the run untouched, so this is the safe way to test from a blank slate. @@ -53,10 +53,24 @@ PTY (ConPTY) → PseudoTerminal → TerminalBridge → WebView2 (xterm.js) |---|---| | `SessionManager` | CRUD for ShellSession models | | `StateService` | JSON persistence → `%AppData%/CodeShellManager/state.json` | -| `SearchService` | SQLite FTS5 search of all terminal output | +| `SearchService` | SQLite FTS5 search of all terminal output; also owns the `project_notes` table | | `ColorService` | FNV-1a hash of folder path → 12-color palette | | `GitService` | Async `git branch --show-current` + `git status --porcelain` | | `AlertDetector` | Pattern matching for Claude prompts/approvals | +| `CommandPresetsService` | Launch presets + in-session shortcuts | +| `ClaudeSessionService` | Detects `claude` invocations; finds last `--resume` session id under `~/.claude/projects/` | +| `UpdateService` | GitHub Releases version check; caches result for 24h at `%AppData%/CodeShellManager/update-cache.json` | +| `ImportExportService` | Read/write a full `AppState` to a JSON file (settings + sessions backup) | +| `ToastHelper` | Tray balloon notifications | +| `SessionRunner` | Per-session owner of `RunInstance` dictionary (run commands runtime) | +| `RunInstance` | One headless PTY-backed run with ANSI-stripped output buffer | +| `RunCommandTemplatesService` | Detects project type (dotnet/cargo/node/python/make) → seed run-command list | +| `WindowsTerminalProfileService` | Reads Windows Terminal `settings.json` from all install variants | +| `BuiltInTerminalSchemes` | Lookup table of WT default color schemes not present in user `settings.json` | +| `SchemeMapper` | WT scheme JSON → xterm.js theme JSON (renames `purple` → `magenta`, rewrites background as `rgba()` when opacity < 1) | +| `CursorShapeMapper` | WT `cursorShape` → xterm.js `cursorStyle` (+ optional forced blink) | +| `PaddingParser` | WT `padding` shorthand (1/2/4 comma ints) → CSS `Npx` shorthand | +| `CommandLineSplitter` | Helper — quote-aware split of a Windows commandline into `(exe, args)` | ## Project Structure @@ -155,6 +169,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 clears the ring at startup (full debug isolation) and never persists changes — `SaveStateAsync` is a no-op in clean mode. + +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 +202,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). @@ -193,7 +223,20 @@ Each session can have a list of "run commands" — labelled command lines invoke **Lifecycle:** All runs are killed on session close, session sleep, and app exit. `SessionViewModel.Dispose()` calls `Runner.Dispose()` which iterates and disposes every instance. `SleepSession` also calls `vm.Runner.StopAll()` defensively before UI teardown. -## Alert / Waiting State +## Per-Session Notes + +Each session gets a collapsible 📝 notepad panel between the terminal toolbar and the terminal. Toggled by the 📝 button on the terminal toolbar; the panel is a docked 160px-high `TextBox` (`Visibility.Collapsed` by default). + +**Storage:** notes are **not** on `ShellSession` and not in `state.json`. They live in the FTS5 SQLite DB owned by `SearchService` in a separate `project_notes` table keyed by `folder_path` (the session's `WorkingFolder`). Two sessions in the same folder share one note; SSH sessions and sessions with no working folder don't get a note (`vm.WorkingFolder` is empty → save is skipped). + +- `SearchService.GetNoteAsync(folderPath)` — `SELECT content FROM project_notes WHERE folder_path = ?` +- `SearchService.SaveNoteAsync(folderPath, content)` — UPSERT on `folder_path`, stamps `updated_at` (ms since epoch) + +**UI lifecycle:** content is lazy-loaded on the first time the panel is opened (`notesLoaded` flag in the toolbar build). Each keystroke restarts a 1-second `System.Threading.Timer` debounce; when it fires, `SaveNoteAsync` is called on the dispatcher thread. No explicit save action — closing the panel or the session just leaves the last debounce to flush. There's no save-on-exit hook, so a note edited in the final ~1s before app close can be lost. + +**Search integration:** `SearchService.SearchAsync` queries notes alongside terminal output — notes use `LIKE %query%` (short free-text, FTS5 overkill) and are tagged `SearchResultType.Note` so the search panel can label them. The note's row in the search panel is keyed by folder, not session. + +**Dormant sessions:** because notes are folder-keyed and live outside `state.json`, a dormant or reopened session in the same folder transparently picks up the existing note on next wake/restore. `AlertDetector` fires `AlertRaised(AlertEvent)` after 1.5s idle when it detects: - **ToolApproval**: Claude asking to run a tool (regex on approval phrases) @@ -231,6 +274,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/installer/CodeShellManager.wxs b/installer/CodeShellManager.wxs index 84d87b0..b664156 100644 --- a/installer/CodeShellManager.wxs +++ b/installer/CodeShellManager.wxs @@ -53,6 +53,9 @@ + + + + + false + + @@ -42,6 +53,10 @@ PreserveNewest true + + PreserveNewest + true + PreserveNewest true diff --git a/src/CodeShellManager/MainWindow.xaml b/src/CodeShellManager/MainWindow.xaml index 7c3ee6b..e602ed5 100644 --- a/src/CodeShellManager/MainWindow.xaml +++ b/src/CodeShellManager/MainWindow.xaml @@ -66,6 +66,7 @@ +