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
48 changes: 43 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ dotnet run --project src/CodeShellManager/CodeShellManager.csproj

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

### 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.
- `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.

## Architecture

### Key layers
Expand All @@ -26,7 +37,7 @@ PTY (ConPTY) β†’ PseudoTerminal β†’ TerminalBridge β†’ WebView2 (xterm.js)
```

- **PseudoTerminal** (`Terminal/PseudoTerminal.cs`): Windows ConPTY wrapper, P/Invoke only
- **TerminalBridge** (`Terminal/TerminalBridge.cs`): Routes bytes between PTY and xterm.js via WebView2 messages
- **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 Down Expand Up @@ -55,7 +66,7 @@ src/CodeShellManager/
β”œβ”€β”€ MainWindow.xaml / .cs # Main UI (toolbar, sidebar, terminal grid)
β”œβ”€β”€ Models/
β”‚ β”œβ”€β”€ AppState.cs # AppSettings + AppState (JSON root)
β”‚ β”œβ”€β”€ ShellSession.cs # Session data model (incl. SSH fields + BuildSshArgs)
β”‚ β”œβ”€β”€ ShellSession.cs # Session data model (SSH fields, BuildSshArgs, IsDormant)
β”‚ β”œβ”€β”€ SessionGroup.cs # Group model
β”‚ └── AlertEvent.cs # Alert types: InputRequired, ToolApproval
β”œβ”€β”€ Services/
Expand Down Expand Up @@ -97,13 +108,19 @@ tests/

**Session accent colors** β€” `ColorService.GetHexColor(key)` uses FNV-1a hash to deterministically assign one of 12 colors. For local sessions the key is `WorkingFolder`; for SSH sessions it is `user@host`. Used as sidebar stripe + terminal toolbar top border.

**Active-terminal highlight** β€” every terminal pane is wrapped in an outer "active ring" Border (constant 2px thickness, transparent by default) so toggling it doesn't shift content. `UpdateActiveTerminalHighlight` (called from `UpdateSidebarActiveState`, which fires on every `MainViewModel.ActiveSession` change) paints the ring of the active session's pane in its accent color and clears all others. The ring's accent hex is stashed on `Border.Tag` at build time so the highlight method doesn't need to look up the VM.

## Session Lifecycle

1. User clicks **οΌ‹ New Session** β†’ `NewSessionDialog` modal (Local or Remote SSH)
2. `SessionManager.CreateSession()` creates `ShellSession` model; caller copies SSH fields if remote
3. `LaunchSessionAsync()` creates: `SessionViewModel` β†’ `WebView2` β†’ `TerminalBridge` β†’ `PseudoTerminal`
4. `OutputIndexer` indexes all output to SQLite; `AlertDetector` watches for prompts
5. On close: `Dispose()` chain cleans up PTY, bridge, indexer, detector
5. Termination paths:
- **Close** (`vm.CloseCommand`) β†’ `MainViewModel.OnSessionCloseRequested` β†’ `vm.Dispose()` + remove from `Sessions` + `SessionManager.RemoveSession()`. Session is gone from `state.json`.
- **Sleep** (`SleepSession(vm)`) β†’ `vm.Dispose()` + remove from `Sessions` but **keep** the `ShellSession` in `SessionManager` with `IsDormant = true`. A muted dormant sidebar entry replaces the active one.
- **Wake** (`WakeSessionAsync(session)`) β†’ re-runs `LaunchSessionAsync(session, restoring: true)` β€” same path as restore-on-startup.
6. On app close: `_vm.SaveStateAsync()` flushes `_sessionManager.Sessions` (live + dormant) to `state.json` (unless `--clean`).

## SSH Remote Sessions

Expand All @@ -117,6 +134,22 @@ Remote sessions use the system `ssh` client as the PTY command β€” no extra libr
- `SessionViewModel.RefreshGitInfoAsync()` early-returns for remote sessions (no local working folder)
- SSH fields serialize to `state.json` automatically β€” sessions restore and relaunch on next startup

## 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.

**UI:**
- πŸ’€ button appears in both the sidebar action panel (next to βœ•) and the terminal toolbar.
- Dormant entries render at the bottom of the sidebar with a muted (55% opacity) appearance. Clicking anywhere on a dormant entry wakes it; the small βœ• on a dormant entry permanently deletes (with confirmation).

**Implementation (`MainWindow.xaml.cs`):**
- `SleepSession(vm)` β€” sets `session.IsDormant = true`, removes from `_vm.Sessions` directly (bypassing `CloseCommand` so the `ShellSession` is **not** removed from `SessionManager`), disposes the VM, and calls `AddDormantSidebarItem(session)`.
- `WakeSessionAsync(session)` β€” clears `IsDormant`, removes the dormant sidebar entry, then `await LaunchSessionAsync(session, restoring: true)`. On launch failure it restores the dormant entry.
- `BuildDormantSidebarItem(ShellSession)` β€” builds a static (no-VM) sidebar Border with muted accent stripe + πŸ’€ icon. Click handler resolves to `WakeSessionAsync`.
- Dormant entries are tracked in `_dormantSidebarItems: Dictionary<string, Border>` so `RebuildSidebarOrder` (called after drag-reorder) can re-append them at the bottom.
- `OnLoaded` partitions saved sessions: dormant ones go through `AddDormantSidebarItem`; live ones through `LaunchSessionAsync`.
- The empty-state placeholder hides whenever `_vm.Sessions.Count > 0` **or** `_dormantSidebarItems.Count > 0`.

## Alert / Waiting State

`AlertDetector` fires `AlertRaised(AlertEvent)` after 1.5s idle when it detects:
Expand All @@ -140,12 +173,15 @@ Remote sessions use the system `ssh` client as the PTY command β€” no extra libr

Persisted in `state.json`. Key settings:
- `AutoRestoreSessions` β€” restore open sessions on next launch
- `AutoResumeClaude` β€” when restoring, append `--resume <sessionId>` to claude commands so the prior conversation is picked up. Toggle off if you want fresh sessions on restart.
- `ShowGitBranch` β€” show `βŽ‡ branch` in sidebar
- `ShowTerminalStatusDot` β€” show status dot in terminal toolbar
- `SearchCollapseAfterNavigate` β€” auto-close search after clicking result
- `MaxSearchResults` β€” FTS5 result limit (default 100)
- `DefaultWorkingFolder` / `DefaultCommand` β€” pre-fill new session dialog

**Layout persistence**: `AppState.LastLayout` (string, e.g. `"TwoByTwo"`) persists the active grid layout. On startup, `MainViewModel.LoadStateAsync` parses it into `Layout`, which fires `MainViewModel.PropertyChanged`; the `MainWindow` constructor subscribes and syncs `_currentLayout` + calls `RefreshTerminalLayout`, so the saved layout is what the user sees on relaunch.

## Keyboard Shortcuts

| Key | Action |
Expand Down Expand Up @@ -182,7 +218,9 @@ The tag value overrides the csproj `<Version>` at publish time (`-p:Version=` fl
## Known Conventions

- All WPF color literals use Catppuccin Mocha hex values β€” do not introduce system colors
- Sidebar items and terminal wrappers are built entirely in code-behind (`BuildSidebarItem`, `BuildTerminalWrapper`) β€” not in XAML templates, to keep imperative logic centralized
- `_sessionUi` dictionary maps `sessionId β†’ (webView, terminalWrapper, sidebarItem)` β€” the source of truth for all session UI references
- Sidebar items and terminal wrappers are built entirely in code-behind (`BuildSidebarItem`, `BuildTerminalWrapper`, `BuildDormantSidebarItem`) β€” not in XAML templates, to keep imperative logic centralized
- `_sessionUi` dictionary maps `sessionId β†’ (webView, terminalWrapper, sidebarItem)` β€” the source of truth for live session UI. `_dormantSidebarItems` (`sessionId β†’ Border`) tracks the parallel set for sleeping sessions.
- The `terminalWrapper` returned by `BuildTerminalWrapper` is actually the **outer active-ring Border**, with the original accent-stripe wrapper nested inside. `_sessionUi[id].terminalWrapper` therefore points at the ring; the highlight method toggles its `BorderBrush`.
- Use `Dispatcher.Invoke()` for all UI updates from background threads (PTY read loop, git queries, alert timer)
- PTY output flows: `PseudoTerminal` β†’ `TerminalBridge.RawOutputReceived` β†’ both `OutputIndexer.Feed()` and `AlertDetector.Feed()` in parallel
- `MainViewModel.SaveStateAsync` is a no-op when `App.CleanStart` is true; any code path that needs to "remember" something across runs must go through this method, so honoring `--clean` is automatic.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ 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)
- **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
- **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.
- **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>`)
- **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
- **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 @@ -53,6 +55,12 @@ cd CodeShellManager
dotnet run --project src/CodeShellManager/CodeShellManager.csproj
```

### Command-line flags

| Flag | Effect |
|------|--------|
| `--clean` | Start with no preloaded sessions and skip writing `state.json` for the run. Useful when developing β€” your saved sessions/settings are left untouched. |

## Keyboard Shortcuts

| Key | Action |
Expand Down
12 changes: 12 additions & 0 deletions src/CodeShellManager/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
using System.Linq;

namespace CodeShellManager;

public partial class App : System.Windows.Application
{
public static System.Windows.Forms.NotifyIcon? TrayIcon { get; private set; }

/// <summary>
/// When true (set by passing <c>--clean</c> on the command line), the app starts
/// with no preloaded sessions and persists no state changes for this run. Useful
/// for debugging: prior <c>state.json</c> contents are left untouched.
/// </summary>
public static bool CleanStart { get; private set; }

public static string LogPath { get; } = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
"CodeShellManager", "crash.log");
Expand All @@ -12,6 +21,9 @@ protected override void OnStartup(System.Windows.StartupEventArgs e)
{
base.OnStartup(e);

CleanStart = e.Args.Any(a =>
string.Equals(a, "--clean", System.StringComparison.OrdinalIgnoreCase));

// Catch unhandled exceptions and write to log
DispatcherUnhandledException += (_, ex) =>
{
Expand Down
1 change: 1 addition & 0 deletions src/CodeShellManager/Assets/terminal.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
else if (msg.type === 'clear') term.clear();
else if (msg.type === 'focus') { term.focus(); fitAddon.fit(); }
else if (msg.type === 'fit') { fitAddon.fit(); term.focus(); }
else if (msg.type === 'paste') term.paste(msg.data);
else if (msg.type === 'setOptions') {
const opts = msg.options;
if (opts.fontFamily !== undefined) term.options.fontFamily = opts.fontFamily;
Expand Down
Loading
Loading