# Hot Exit Improvements: Behavioral PRD & Implementation Plan

This document covers the design and implementation plan for addressing issues
#1231, #1232, #1233, #1234, #1237 (and umbrella #1238). Issues #1235 (detach
vs quit) and #1236 (implicit sessions) are explicitly deferred.

---

## 1. Behavioral PRD

### 1.1 Design Principles

1. **Never silently discard unsaved data.** Unsaved buffers must only disappear
   through explicit user action (closing a tab with confirmation). Every other
   path — exit, reopen, session switch, CLI invocation — must preserve them.

2. **Restore exactly what was there.** On relaunch, the user should see the
   same tabs in the same order with the same active buffer. No extra buffers
   injected, no buffers missing, no reordering.

3. **CLI files are additive.** Specifying file arguments on the command line
   adds those files to the restored workspace; it never replaces it.

4. **Session isolation.** Each named session has its own recovery data. One
   session's exit must never clobber another session's unsaved state.

5. **Inform, don't surprise.** When state is restored, tell the user. When
   recovery is skipped (e.g., file changed on disk), tell the user.

### 1.2 UX Flows

#### Flow A: Launch with no arguments, no session

```
User runs: fresh
            ↓
    ┌─ Workspace file exists for CWD?
    │   YES → Restore workspace (tabs, order, splits, cursors, scroll)
    │         → Apply hot exit recovery to restored buffers
    │         → Apply hot exit recovery for unnamed buffers
    │         → Show status: "Restored N buffer(s) from previous session"
    │         → Active buffer = whatever was active when workspace was saved
    │         → Do NOT create extra unnamed buffer
    │
    │   NO  → Has recovery files (crash recovery)?
    │         YES → recover_all_buffers() (existing crash path)
    │         NO  → Create single unnamed buffer (default)
    └─
```

**Change from current behavior**: When workspace restore succeeds and there are
no CLI files, the editor no longer creates an extra empty unnamed buffer
(fixes #1231).

#### Flow B: Launch with file arguments, no session

```
User runs: fresh file1.txt file2.txt
            ↓
    ┌─ Workspace file exists for CWD?
    │   YES → Restore workspace (all tabs, order, splits, cursors)
    │         → Apply hot exit recovery to all restored buffers
    │         → Then open CLI files:
    │           - If file already open (from workspace), just focus it
    │           - If file is new, append tab at end of active split's tab bar
    │         → Set active buffer = last CLI-specified file
    │         → Show status: "Restored N buffer(s), opened M file(s)"
    │
    │   NO  → Open CLI files in order specified
    │         → Apply hot exit recovery to those files (existing behavior)
    │         → Also recover any non-file-backed (unnamed) buffers from recovery
    │         → Active buffer = last CLI file
    └─
```

**Change from current behavior**: Previously, launching with file arguments
would skip workspace restore entirely, discarding all previously-open tabs.
Now workspace is always restored first, CLI files are layered on top
(fixes #1232).

#### Flow C: Launch with named session, no file arguments

```
User runs: fresh -a mysession
            ↓
    ┌─ Server for 'mysession' already running?
    │   YES → Connect to server (hot exit irrelevant, buffers are live)
    │
    │   NO  → Start new server for 'mysession'
    │         → Look for workspace in session-scoped workspace dir
    │         → Look for recovery data in session-scoped recovery dir
    │         → Restore workspace + apply session-scoped hot exit recovery
    │         → Show status: "Restored session 'mysession' (N buffer(s))"
    └─
```

**Change from current behavior**: Recovery data is now stored per-session
rather than globally. Session 'foo' exiting no longer overwrites session
'bar's recovery files (fixes #1233).

#### Flow D: Launch with named session AND file arguments

```
User runs: fresh -a mysession file.txt
            ↓
    ┌─ Server for 'mysession' already running?
    │   YES → Connect to server
    │         → Send OpenFiles control message for file.txt
    │         → Server opens file.txt (appends to tab bar, focuses it)
    │
    │   NO  → Start new server (same as Flow C)
    │         → Restore workspace + hot exit recovery
    │         → Open file.txt:
    │           - If already in restored tabs, focus it
    │           - Otherwise append to tab bar and focus
    └─
```

**Change from current behavior**: File arguments now work correctly with
session restore rather than replacing the session state (fixes #1237).

#### Flow E: Exit (clean shutdown)

```
User quits (Ctrl+Q or via command palette "Quit")
            ↓
    1. Capture workspace state:
       - Tab order (explicit ordered list, not iteration order)
       - Active tab per split
       - Cursor positions, scroll positions
       - Split layout, file explorer state
       - Unnamed buffer references (recovery IDs)

    2. Save recovery data for dirty buffers:
       - File-backed modified buffers → recovery files
       - Unnamed modified buffers → recovery files
       - Recovery dir = session-scoped if in session mode

    3. Save workspace to:
       - Session-scoped workspace dir if in session mode
       - CWD-scoped workspace dir if in standalone mode

    4. Clean up session lock file
```

**Change from current behavior**: Tab order is now explicitly serialized as
an ordered array rather than relying on HashMap iteration order (fixes #1234).
Recovery files are written to a session-scoped directory when running in
session mode.

#### Flow F: File changed on disk since hot exit

```
During hot exit recovery, for each file-backed buffer:
            ↓
    ┌─ File mtime matches recovery metadata's original_mtime?
    │   YES → Apply recovery (restore unsaved changes)
    │
    │   NO  → Do NOT silently skip
    │         → Open the file with current disk contents
    │         → Show warning in status bar:
    │           "file.txt changed on disk; unsaved changes not restored"
    │         → Keep recovery file (don't delete) for manual inspection
    └─
```

**Change from current behavior**: Currently, when mtime doesn't match, the
recovery is silently skipped and the recovery file is deleted. The user never
knows they had unsaved changes. Now the user is warned and the recovery file
is preserved.

### 1.3 Configuration

The two existing Rust config fields `persist_unnamed_buffers` and `hot_exit`
(both in `EditorConfig`, `config.rs`) are merged into a single field:

```rust
/// Whether to preserve unsaved changes in all buffers (file-backed and unnamed)
/// across editor sessions (VS Code "hot exit" behavior).
/// When enabled, no "Save changes?" prompt on clean exit.
///
/// Default: true
#[serde(default = "default_true")]
#[schemars(extend("x-section" = "Recovery"))]
pub hot_exit: bool,
```

The `persist_unnamed_buffers` field is removed from the struct. For backward
compatibility in user JSON config files, a `#[serde(alias = "persist_unnamed_buffers")]`
attribute is added to `hot_exit` so old configs still parse.

After making this change, the JSON config schema must be regenerated:
```sh
./scripts/gen_schema.sh
```

### 1.4 Status Messages

| Scenario | Message |
|---|---|
| Workspace restored | `"Restored N buffer(s) from previous session"` |
| Session restored | `"Restored session 'name' (N buffer(s))"` |
| Recovery skipped (mtime) | `"file.txt changed on disk; unsaved changes not restored"` |
| Crash recovery | (existing UI — recovery prompt unchanged) |

---

## 2. Architecture Changes

### 2.1 Session-Scoped Recovery Storage

**Current layout:**
```
~/.local/share/fresh/
├── recovery/           ← global, shared by all instances
│   ├── session.lock
│   ├── {id}.meta.json
│   └── {id}.chunk.N
└── workspaces/         ← already CWD-scoped
    └── {encoded_cwd}.json
```

**New layout:**
```
~/.local/share/fresh/
├── recovery/
│   ├── default/        ← standalone mode (no -a), scoped by CWD hash
│   │   ├── {cwd_hash}/
│   │   │   ├── session.lock
│   │   │   ├── {id}.meta.json
│   │   │   └── {id}.chunk.N
│   │   └── ...
│   └── sessions/       ← session mode (-a NAME)
│       ├── {session_name}/
│       │   ├── session.lock
│       │   ├── {id}.meta.json
│       │   └── {id}.chunk.N
│       └── ...
├── workspaces/         ← standalone mode (unchanged, CWD-scoped)
│   └── {encoded_cwd}.json
└── session-workspaces/ ← session mode workspace files
    └── {session_name}.json
```

**Migration**: On startup, if old-style recovery files exist in `recovery/`
(flat layout with no `default/` or `sessions/` subdirs), migrate them into
`recovery/default/{cwd_hash}/` based on the current working directory. This
is a one-time migration.

### 2.2 RecoveryStorage Scoping

`RecoveryStorage::new()` currently always points to `~/.local/share/fresh/recovery/`.
We add a constructor that accepts a scope:

```rust
pub enum RecoveryScope {
    /// Standalone mode: scoped by working directory
    Standalone { working_dir: PathBuf },
    /// Session mode: scoped by session name
    Session { name: String },
}

impl RecoveryStorage {
    pub fn with_scope(scope: RecoveryScope) -> io::Result<Self> { ... }
}
```

### 2.3 Workspace Storage for Sessions

`Workspace::load()` and `Workspace::save()` currently use
`get_workspace_path(working_dir)`. For session mode, we add:

```rust
pub fn get_session_workspace_path(session_name: &str) -> io::Result<PathBuf> {
    let dir = get_data_dir()?.join("session-workspaces");
    fs::create_dir_all(&dir)?;
    Ok(dir.join(format!("{}.json", session_name)))
}
```

And `Workspace` gets `load_session(name)` / `save_session(name)` methods.

### 2.4 Tab Order Serialization Fix

`SerializedSplitViewState.open_tabs` already exists as `Vec<SerializedTabRef>`,
which is an ordered list. The bug is in `capture_workspace()` where the tab
list is built. Need to verify that `serialize_split_view_state()` preserves
the visual tab order from the split manager rather than iterating a HashMap.

---

## 3. Implementation Plan

### Phase 1: Bug Fixes (no architectural changes)

#### Task 1.1: Fix extra unnamed buffer on restore (#1231)

**Files**: `main.rs` (`handle_first_run_setup`)

**Problem**: When workspace restore succeeds AND no CLI files are given, an
empty unnamed buffer (BufferId 1) is left open alongside the restored tabs.

**Fix**: After `try_restore_workspace()` succeeds, check if the initial
unnamed buffer (BufferId 1) is still empty/unmodified/unnamed. If so, close
it and remove it from the tab bar. This is the same pattern used by
`open_stdin_buffer()` which replaces the initial buffer when stdin is piped.

Specifically, in `apply_workspace()` (workspace.rs), after files are opened
and tabs rebuilt, check if BufferId 1 is in the tab bar and is
empty/unmodified/unnamed. If the workspace had any files to restore, remove
BufferId 1 from the split's open_tabs.

#### Task 1.2: Fix buffer ordering on restore (#1234)

**Files**: `workspace.rs` (`capture_workspace`, `apply_workspace`)

**Problem**: Buffer order after hot exit appears random.

**Investigation**: Verify that `capture_workspace()` serializes
`open_tabs` in visual tab-bar order. The `split_manager` should provide tabs
in order. If it does, the bug might be in `apply_workspace()` where files are
opened — opening files via `open_file_internal()` may append them in arbitrary
order and then tab reordering doesn't happen.

**Fix**: In `apply_workspace()`, after opening all files, explicitly set the
tab order for each split to match the serialized `open_tabs` order. The
`SplitViewState` should have a method to reorder tabs to match a given
sequence of BufferIds.

#### Task 1.3: Don't discard hot exit recovery when CLI files specified (#1232)

**Files**: `main.rs` (`handle_first_run_setup`)

**Problem**: `fresh file.txt` discards all previously-open buffers from the
workspace.

**Current flow**:
1. `try_restore_workspace()` is called (may restore workspace)
2. CLI files are queued
3. `recover_all_buffers()` runs for crash recovery only

**Fix**: The workspace restore already happens before CLI files are queued
(line 746). The issue is that workspace restore might not be happening, OR
the workspace is restored but then the initial unnamed buffer crowds the
tab bar. Ensure:
1. `try_restore_workspace()` always runs (currently it does)
2. CLI files are opened AFTER workspace restore, appended to tab bar
3. Active buffer is set to the last CLI file
4. The initial empty unnamed buffer is cleaned up (same as Task 1.1)

Also: `recover_all_buffers()` (the crash-recovery path at line 793) currently
opens NEW buffers for recovered files, even if those files weren't in the
workspace. This is correct for crash recovery but creates duplicates if the
workspace also had those files. Need to deduplicate: if a recovered file is
already open (from workspace restore), apply recovery content to the existing
buffer instead of opening a new one.

### Phase 2: Session-Scoped Recovery (#1233)

#### Task 2.1: Add RecoveryScope and scoped storage

**Files**: `services/recovery/storage.rs`, `services/recovery/mod.rs`

Add `RecoveryScope` enum. Modify `RecoveryStorage::new()` to accept an
optional scope. The recovery directory becomes:
- Standalone: `recovery/default/{cwd_hash}/`
- Session: `recovery/sessions/{session_name}/`

Add migration logic: if `recovery/session.lock` exists at the old flat path,
move all files into `recovery/default/{current_cwd_hash}/`.

#### Task 2.2: Pass session context through to RecoveryStorage

**Files**: `app/mod.rs` (Editor constructor), `server/editor_server.rs`,
`main.rs`

The Editor needs to know its recovery scope at construction time. Add a
`recovery_scope: Option<RecoveryScope>` to `EditorOptions` or pass it to
`Editor::with_working_dir()`. In session mode (`editor_server.rs`), set
`RecoveryScope::Session { name }`. In standalone mode, set
`RecoveryScope::Standalone { working_dir }`.

#### Task 2.3: Session-scoped workspace files

**Files**: `workspace.rs`

Add `get_session_workspace_path(session_name)`. In session mode, workspace
save/load uses session name instead of CWD. This means a session always
restores ITS workspace regardless of which directory the client connects from.

Add `Workspace::load_session()` and `Workspace::save_session()` methods,
or parameterize existing `load()`/`save()` with an enum.

#### Task 2.4: Wire session name through server lifecycle

**Files**: `server/editor_server.rs`, `server/daemon/unix.rs`, `main.rs`

Ensure the session name is available to the Editor when running in server
mode. Currently `spawn_server_detached()` passes `--session-name NAME`. The
server startup path should forward this name to the Editor so it can
construct the correct `RecoveryScope`.

### Phase 3: CLI Files + Session Restore (#1237)

#### Task 3.1: Open CLI files within an existing session server

**Files**: `server/editor_server.rs`, `server/protocol.rs`

When `fresh -a mysession file.txt` connects to an ALREADY RUNNING server,
it sends `ClientControl::OpenFiles`. Verify this works correctly — the server
should open the file and focus it. This likely already works but needs testing.

#### Task 3.2: Open CLI files in a freshly-started session

**Files**: `main.rs`, `server/editor_server.rs`

When `fresh -a mysession file.txt` starts a NEW server, the file arguments
need to be forwarded. Currently `spawn_server_detached()` doesn't forward
file arguments to the server process. The client connects and then sends
`OpenFiles` via the control socket.

Verify this path works with workspace restore: the server should restore the
session workspace, THEN process the `OpenFiles` message from the client.
The file should be appended to the tab bar and focused. If the file is
already in the restored workspace, it should just be focused (not duplicated).

### Phase 4: Notifications and Polish

#### Task 4.1: Status bar notification on restore

**Files**: `app/workspace.rs`, `app/mod.rs`

After workspace restore + hot exit recovery completes, set a status message
visible to the user. Use the existing status bar message mechanism
(`set_status_message()`).

#### Task 4.2: Warn when recovery skipped due to mtime mismatch

**Files**: `app/recovery_actions.rs`, `app/workspace.rs`

In `recover_all_buffers()` and `apply_hot_exit_recovery()`, when
`RecoveryResult::OriginalFileModified` is returned, instead of silently
discarding the recovery file:
1. Show a status warning
2. Keep the recovery file on disk (don't call `discard_recovery()`)

Add a method to list preserved-but-skipped recovery files so users can
manually inspect them later.

#### Task 4.3: Merge config settings

**Files**: `config.rs`, `partial_config.rs`, `app/recovery_actions.rs`,
`app/workspace.rs`, `app/prompt_actions.rs`, `app/buffer_management.rs`

1. Remove the `persist_unnamed_buffers` field from `EditorConfig` in
   `config.rs`. Add `#[serde(alias = "persist_unnamed_buffers")]` to `hot_exit`
   so existing user JSON configs that set `persist_unnamed_buffers` still parse.
2. Update `Default` impl to no longer set `persist_unnamed_buffers`.
3. Remove all internal branching that checks `persist_unnamed_buffers`
   separately — use the single `hot_exit` flag everywhere.
4. Update `partial_config.rs` if it mirrors the field.
5. Regenerate JSON config schema: `./scripts/gen_schema.sh`
6. Regenerate TypeScript definitions if the config is exposed to plugins:
   `cargo test -p fresh-plugin-runtime write_fresh_dts_file -- --ignored`

### Phase 5: Testing

Per CONTRIBUTING.md: bug fixes must first reproduce the issue in a failing
test, then add the fix. New user flows must include e2e tests. E2e tests
send keyboard/mouse events and examine rendered output, not internal state.
Use semantic waiting (not timeouts). Tests must be isolated (temp dirs,
internal clipboard).

#### Task 5.1: E2e tests for each flow (reproduce-first for bug fixes)

For each bug fix (Tasks 1.1–1.3), write a failing e2e test first:
- **#1231**: Launch with workspace file present, no CLI args → assert no
  extra unnamed buffer tab in rendered output
- **#1232**: Launch with workspace + CLI file → assert workspace tabs are
  present AND CLI file is focused
- **#1234**: Save workspace with known tab order, relaunch → assert tabs
  render in same order

New flow e2e tests:
- Session-scoped recovery isolation (two sessions, each with own dirty buffers)
- File mtime change → warning message appears in rendered output, recovery
  file not deleted
- `fresh -a session file.txt` → file tab appears in restored session

#### Task 5.2: Migration test

Test that existing flat-layout recovery files are correctly migrated into
the new scoped directory structure on first launch. (Unit test, not e2e —
this is filesystem logic.)

---

## 4. Build & Commit Requirements

Per CONTRIBUTING.md, every commit must pass:
- `cargo check --all-targets`
- `cargo fmt`

After config changes, regenerate schemas:
- `./scripts/gen_schema.sh`
- `cargo test -p fresh-plugin-runtime write_fresh_dts_file -- --ignored` (if plugin API types changed)
- `crates/fresh-editor/plugins/check-types.sh` (TypeScript type check)

---

## 5. Risk Assessment

| Risk | Mitigation |
|---|---|
| Migration corrupts existing recovery files | Migrate by copying, not moving. Only delete originals after confirming copies are readable. |
| Tab order fix regresses other tab behaviors | Existing test suite + new order-specific tests |
| Config merge breaks users who set them independently | `persist_unnamed_buffers` accepted as alias; both `true` = `hot_exit: true` |
| Session workspace path collisions | Session names are user-chosen strings; sanitize for filesystem safety |
| Race between client OpenFiles and server workspace restore | Server should fully complete startup/restore before accepting client connections (already the case — server binds sockets after init) |
