Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e386863
feat(bundle): low-hanging fruit — #51 #53 #54 #55 #39 #46
AThraen May 16, 2026
9de6332
feat(spinner): add boot overlay markup + CSS to xterm host pages
AThraen May 16, 2026
6a3b8d8
feat(spinner): add setBootState/bootDone handlers in terminal-init.js
AThraen May 16, 2026
9a77e22
feat(spinner): add SetBootContext + post setBootState in TerminalBridge
AThraen May 16, 2026
d980f09
fix(spinner): suppress CS0169 on _bootDoneFlag pending Task 4
AThraen May 16, 2026
b6fed0e
feat(spinner): show launching overlay until first PTY byte
AThraen May 16, 2026
e5974b0
feat(spinner): add ShutdownOverlay grid to MainWindow.xaml
AThraen May 16, 2026
7fdffc9
feat(spinner): show ShutdownOverlay during OnClosing teardown
AThraen May 16, 2026
c4b41b5
test: add unit tests for AlertDetector, ColorService, OutputIndexer, …
AThraen May 17, 2026
9d8ece9
refactor(run): extract IPseudoTerminal for SessionRunner testability
AThraen May 17, 2026
1ec2b4b
docs: expand CLAUDE.md Services table + add Per-Session Notes section
AThraen May 17, 2026
6fec182
fix(installer): add app.png to WiX manifest (CI verify-manifest check)
AThraen May 17, 2026
8f31faf
review: address Copilot feedback on PR #63
AThraen May 17, 2026
a1095be
fix: disable Hot Reload + clear groups on --clean
AThraen May 17, 2026
3527cfd
fix(--clean): also clear recently-closed ring for full debug isolation
AThraen May 17, 2026
4d75dbd
chore(vs): commit launchSettings.json with Hot Reload disabled
AThraen May 17, 2026
cf8020c
Merge pull request #63 from umage-ai/feat/bundle-low-hanging-fruit
AThraen May 17, 2026
4e24ea1
docs(spinner): add design spec and implementation plan
AThraen May 17, 2026
abfb23a
docs: refresh CLAUDE.md and README after the bundle merge
AThraen May 17, 2026
c027b7f
Merge pull request #64 from umage-ai/docs/spinner-design
AThraen May 17, 2026
2b2d7ad
feat(sessions): first-class WSL session type alongside Local and SSH
Bitblade May 17, 2026
60de1ab
feat(new-session): folder picker for WSL working folder
Bitblade May 17, 2026
4a49953
feat(sessions): add IsWsl convenience predicate on ShellSession
Bitblade May 17, 2026
bc4b3e6
fix(git): route GitService through wsl.exe for WSL UNC working folders
Bitblade May 17, 2026
fe2b3d0
feat(new-session): WSL browse picker opens at the distro's home folder
Bitblade May 17, 2026
ad657d1
fix(new-session): re-fire name suggestion when distro / folder changes
Bitblade May 17, 2026
e82f6ae
feat(new-session): inherit WSL distro/user/folder from the parent ses…
Bitblade May 17, 2026
736dca4
feat(menu): "Open WSL console here" for WSL sessions
Bitblade May 17, 2026
96003de
fix(worktree): new worktree sessions inherit kind from the parent
Bitblade May 17, 2026
86b73ff
fix(new-session): drop SelectedPath so the WSL picker's Folder field …
Bitblade May 17, 2026
bf8011f
fix(wsl): drain stderr in GetDistroHomeAsync to avoid pipe-buffer dea…
Bitblade May 17, 2026
6c93b67
docs(session-vm): update stale comment on WSL git refresh
Bitblade May 17, 2026
b26accd
feat(run-commands): allow template seeding for WSL sessions
Bitblade May 17, 2026
ef4c73e
fix(recents): persist Kind + WSL fields so reopened WSL sessions stay…
Bitblade May 17, 2026
ae34fbb
fix(git): TranslateUncArgsToLinux handles quoted UNC paths with spaces
Bitblade May 17, 2026
9c2ac28
fix(args): quote distro/user/cwd values in WSL arg builders
Bitblade May 17, 2026
201d61e
fix(new-session): reject non-WSL paths from the WSL folder picker
Bitblade May 17, 2026
dfc0293
fix(new-session): resolve \$HOME eagerly when WslWorkingFolder is blank
Bitblade May 17, 2026
73694d0
fix(wsl): honor "Never throws" contract on discovery helpers
Bitblade May 17, 2026
08140e3
fix(wsl): Parse handles distro names with spaces
Bitblade May 17, 2026
564a773
fix(wsl): QuoteForCmd the distro/user in GetDistroHomeAsync too
Bitblade May 17, 2026
c7ef555
fix(run-commands): switch WSL run-args to Windows-style double quotes
Bitblade May 17, 2026
e0044f0
fix(git): TranslateLinuxPathsToUnc allows spaces in path tail
Bitblade May 17, 2026
a376a7a
refactor(paths): use Path.GetFileName instead of hand-rolled LeafName
Bitblade May 17, 2026
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
73 changes: 68 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ dotnet run --project src/CodeShellManager/CodeShellManager.csproj

**Requirements:** .NET 10 SDK, Windows 10/11 (uses ConPTY + WebView2)

### Visual Studio: Hot Reload disabled

`Properties/launchSettings.json` ships with `"hotReloadEnabled": false`, and the csproj sets `<MetadataUpdaterSupport>false</MetadataUpdaterSupport>` in Debug. Both are workarounds for a `System.ExecutionEngineException` that crashes the app on F5 under .NET 10.0.8 + VS 18 — `Microsoft.Extensions.DotNetDeltaApplier.dll` faults during its own startup before any managed code runs. Ctrl+F5 (Start Without Debugging) is unaffected either way. **Remove both when the runtime bug is fixed upstream.**

### Command-line flags

| Flag | Effect |
|---|---|
| `--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.

Expand All @@ -36,7 +40,7 @@ PTY (ConPTY) → PseudoTerminal → TerminalBridge → WebView2 (xterm.js)
AlertDetector → SessionViewModel.RaiseAlert()
```

- **PseudoTerminal** (`Terminal/PseudoTerminal.cs`): Windows ConPTY wrapper, P/Invoke only
- **PseudoTerminal** (`Terminal/PseudoTerminal.cs`): Windows ConPTY wrapper, P/Invoke only. Implements `IPseudoTerminal` (`Terminal/IPseudoTerminal.cs`) — the minimum surface needed by `RunInstance` (`DataReceived`, `Exited`, `ExitCode`, `Start`). Tests inject a fake via the `internal RunInstance(item, Func<IPseudoTerminal>)` constructor.
- **TerminalBridge** (`Terminal/TerminalBridge.cs`): Routes bytes between PTY and xterm.js via WebView2 messages. Surfaces accelerator keys (Ctrl-combos, F-keys, Esc) via `_webView.PreviewKeyDown` — the newer WPF WebView2 wrapper forwards accelerators through standard key events rather than a separate `CoreWebView2Controller.AcceleratorKeyPressed`. Bridge re-raises them as `AcceleratorKeyPressed` so `MainWindow.OnBridgeAcceleratorKey` can run global shortcuts even when the terminal has focus.
- **OutputIndexer** (`Terminal/OutputIndexer.cs`): Async channels → SQLite, strips ANSI
- **AlertDetector** (`Services/AlertDetector.cs`): Regex on raw PTY output, fires after 1.5s idle
Expand All @@ -53,10 +57,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

Expand Down Expand Up @@ -155,6 +173,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.
Expand All @@ -175,7 +206,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<RunCommandItem> { Id, Label, CommandLine, IsDefault }`. Exactly one item has `IsDefault=true`; see `RunCommandItem.EnsureSingleDefault`. Persisted to `state.json`.
**Data:** `ShellSession.RunCommands: List<RunCommandItem> { 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 <utf16le-b64>` (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).

Expand All @@ -193,7 +227,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)
Expand All @@ -206,6 +253,16 @@ Each session can have a list of "run commands" — labelled command lines invoke

`AlertDetector.NotifyUserInteracted()` clears alert state on user input.

## Session Spinners

Two overlays cover launch and shutdown so the user sees progress instead of a blank pane.

**Launch overlay (per session)** lives in `Assets/terminal.html` and `Assets/terminal-transparent.html` as a CSS-animated rotating SVG arc with a phase label. Visible by default; `TerminalBridge` posts `setBootState` after `NavigationCompleted` (label = `Starting {cmd}…` for local, `Connecting to {host}…` for SSH; accent = session color) and `bootDone` on the first PTY byte (via `OnPtyData → PostBootDoneIfNeeded`, race-safe via `Interlocked.CompareExchange`). An 8-second fallback timer scheduled in `NavCompleted` also calls `PostBootDoneIfNeeded` so silent sessions and slow SSH handshakes don't lock the user out of the pane.

**Shutdown overlay (app-level)** is a `Grid x:Name="ShutdownOverlay"` on `MainWindow.xaml` with a `Storyboard`-rotated `Path`. `OnClosing` shows it then `await Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background)` to force a render pass before the existing synchronous session-disposal loop blocks the UI thread.

Full design: `docs/superpowers/specs/2026-05-16-session-spinners-design.md`.

## Search

- All PTY output is stripped of ANSI and indexed to SQLite FTS5 by `OutputIndexer`
Expand All @@ -231,6 +288,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 |
Expand All @@ -250,6 +309,10 @@ Unit tests cover model logic (`ShellSession`, etc.) and run headless. UI tests r

`ShellSession.BuildSshArgs()` is `internal` — accessible from tests via `[assembly: InternalsVisibleTo("CodeShellManager.Tests")]` in `AssemblyInfo.cs`.

**`IPseudoTerminal` testability seam.** `PseudoTerminal` implements `IPseudoTerminal` (in `Terminal/IPseudoTerminal.cs`), and `RunInstance` / `SessionRunner` both expose an `internal` constructor that accepts a `Func<IPseudoTerminal>` factory. Production code uses the parameterless public ctors which default to `() => new PseudoTerminal()`; tests pass a hand-rolled `FakePseudoTerminal` to exercise the run-command lifecycle (Run, Stop, Dismiss, kill-and-restart, 1MB output-buffer cap) without spawning a real ConPTY child. Keep the interface surface minimal — only what `RunInstance` actually calls (`DataReceived`, `Exited`, `ExitCode`, `Start`).

**SearchService tests** open a fresh file-backed SQLite at `Path.GetTempPath()` per test for isolation. The test class is `IDisposable` and clears the connection pool (`SqliteConnection.ClearAllPools()`) before deleting the file on Windows. Seed `session_history` rows with explicit timestamps rather than `Task.Delay` — Windows' 15.6ms timer granularity makes wall-clock-based ordering flaky on CI.

## Releases

CI/CD is in `.github/workflows/build.yml`. Releases are triggered by pushing a `v*.*.*` tag:
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@ Built with WPF + [xterm.js](https://xtermjs.org/) + Windows ConPTY for full pseu
## Features

- **Multi-terminal grid** — run up to 18 sessions simultaneously in configurable layouts (1, 2, 3, 4, 6 columns; 2×2, 6×2, 6×3 grids); the active pane is highlighted with a 2px accent ring so it's easy to spot
- **Sidebar groups** — organise sessions into named groups with their own color and filter strip; bulk actions (sleep / close / re-group) operate on the active group
- **Sleep & wake** — 💤 button parks a session: PTY torn down, but the session (and its notes) stays in the sidebar so you can wake it later from where you left off. Great when you have many long-running projects but only need a few live at once.
- **Recently closed** — Ctrl+Shift+T reopens the last-closed session (browser convention); the New Session dialog also lists the last 10 closed sessions for one-click revival
- **Per-session run commands** — define a list of labelled commands per session (Test, Build, Watch…); ▶ runs the default, F5 / Shift+F5 run/stop it, output streams into a side drawer without touching the parent terminal. Optional post-run URL opens in your browser on exit code 0.
- **Full-text search** — all terminal output indexed to SQLite FTS5; instant search across every session, ever
- **Per-project notepad** — collapsible 📝 notes panel on every terminal, auto-saved and searchable
- **Alert detection** — detects when Claude is waiting for input or tool approval; green/orange dot indicators
- **Git status** — shows branch and dirty state in the sidebar per session
- **Session rename** — double-click any session name or click ✏ to rename inline
- **Auto-resume** — automatically resumes the last Claude Code session when restoring on startup (`--resume <id>`); toggleable in Settings
- **SSH remote sessions** — connect to remote hosts using your existing SSH config; sessions persist across restarts
- **Windows Terminal profile import** — opt-in import of profiles from Windows Terminal's `settings.json`; pick a profile in the New Session dialog to stamp its font, color scheme, cursor and padding onto the new terminal
- **Launch & shutdown spinners** — every starting session shows a brief overlay (`Starting <cmd>…` or `Connecting to <host>…`) until the first PTY byte arrives; closing the window shows a "Shutting down…" overlay during session disposal
- **WSL sessions** — first-class session type for any installed WSL distro: distro picker (auto-detected via `wsl -l -v`), Linux working folder, optional `-u` user override; git status works via the `\\wsl$\<distro>` UNC view
- **Session history** — clicking a search result from a closed session offers to relaunch it
- **Configurable launch commands** — customise the commands available in the New Session dialog
- **Claude badge** — sessions running `claude` commands get a visual indicator
Expand Down Expand Up @@ -86,9 +92,13 @@ dotnet run --project src/CodeShellManager/CodeShellManager.csproj
| Key | Action |
|-----|--------|
| `Ctrl+T` | New session |
| `Ctrl+Shift+T` | Reopen the most-recently-closed session |
| `Ctrl+Alt+T` | Duplicate the active session |
| `Ctrl+W` | Close active session |
| `Ctrl+F` | Toggle search |
| `Ctrl+Tab` | Cycle sessions |
| `Ctrl+Tab` / `Ctrl+Shift+Tab` | Cycle sessions |
| `F5` | Run the active session's default run-command |
| `Shift+F5` | Stop the active session's running run-command |
| `Escape` (in search) | Close search panel |

## Layout Options
Expand Down
Loading