fix(tui): show the interrupted-session hint inside the TUI (Refs #7095)

A plain `codewhale` launch deliberately starts fresh. When it found an
in-flight session snapshot (a session whose turn never finished, which
exists only under ~/.codewhale/sessions/checkpoints/), the only pointer to
`codewhale --continue` was an eprintln! printed just before the TUI took
the alternate screen, so a user who closed the terminal mid-task reopened
to an empty session and no visible way back (#7095).
`preserve_interrupted_checkpoint_for_explicit_resume` now returns that
message and the launch passes it as the TUI startup notice; an auto-resume
receipt still takes precedence. Its doc comment no longer claims
checkpoints are persisted as sessions (untrue since #4479).

Also applies rustfmt to the `mod usage_persistence` line 98104b9737
left out of order, the only `cargo fmt --all -- --check` drift on
origin/main at a19f2d8885.

Not changed here (follow-ups): the SIGHUP/SIGTERM path exits without
flushing the persistence actor; the resume picker and `codewhale sessions`
do not list checkpoint-only sessions; the 24h checkpoint prune can remove
them.

Evidence (on an export of #7108's merged head cee2d8ab7c plus these lib.rs
hunks, since origin/main codewhale-tui did not compile at the time):
- scripts/dev-cargo.sh check -p codewhale-tui --lib --locked: rc=0, no
  errors or warnings.
- scripts/dev-cargo.sh test -p codewhale-tui --lib --locked -- checkpoint:
  test result: ok. 79 passed; 0 failed (includes
  plain_launch_preserves_checkpoint_but_starts_fresh,
  plain_launch_consumes_legacy_checkpoint_after_preserving_it,
  continue_without_interactive_terminal_leaves_checkpoint_for_a_real_launch).
- cargo fmt --all -- --check on origin/main + this commit: clean.
Not run: a live TUI launch with a real interrupted session.

CW-7525.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-27 15:21:44 -07:00
1 parent b32caa6a17
commit 22e341107e
3 files changed
+29 -29

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Interrupted session hint stays visible (#7095):** when a plain `codewhale` launch finds an in-flight session snapshot, the "Run `codewhale --continue` to resume it" hint now appears in the TUI's status line instead of being printed to the terminal just before the TUI covered it.
- **Crate split S24a (internal):** the workshop byte-budget slot moves to `codewhale-runtime`, `AutoRouteDecisionEvidence` to `codewhale-models`, and cost-status session persistence to a tui `usage_persistence` module, so billing no longer reaches the session manager or model routing. No behavior change. (CW-6129)
- **Nested and deletable threads (Runtime API):** a fork (including a backtrack fork) records `parent_thread_id`, `POST /v1/threads` accepts one, and `GET /v1/threads/summary` returns it so clients can nest child conversations. `DELETE /v1/threads/{id}` removes an idle thread with its turns, items, events and goal, and its saved session unless another thread shares it or a terminal holds it open; a running thread answers 409.
- **Hunk actions in Review (Runtime API):** `POST /v1/git/stage`, `/v1/git/unstage` and `/v1/git/discard` take `hunks` (the `@@ … @@` headers from `GET /v1/diff`) with one path and apply only those hunks through `git apply`, honouring the same `expect` preconditions as whole-path writes. A hunk that no longer exists answers `409 git_state_changed` with `stale_hunks`, and nothing is written unless git accepts the whole patch.
+1
View File
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Interrupted session hint stays visible (#7095):** when a plain `codewhale` launch finds an in-flight session snapshot, the "Run `codewhale --continue` to resume it" hint now appears in the TUI's status line instead of being printed to the terminal just before the TUI covered it.
- **Crate split S24a (internal):** the workshop byte-budget slot moves to `codewhale-runtime`, `AutoRouteDecisionEvidence` to `codewhale-models`, and cost-status session persistence to a tui `usage_persistence` module, so billing no longer reaches the session manager or model routing. No behavior change. (CW-6129)
- **Nested and deletable threads (Runtime API):** a fork (including a backtrack fork) records `parent_thread_id`, `POST /v1/threads` accepts one, and `GET /v1/threads/summary` returns it so clients can nest child conversations. `DELETE /v1/threads/{id}` removes an idle thread with its turns, items, events and goal, and its saved session unless another thread shares it or a terminal holds it open; a running thread answers 409.
- **Hunk actions in Review (Runtime API):** `POST /v1/git/stage`, `/v1/git/unstage` and `/v1/git/discard` take `hunks` (the `@@ … @@` headers from `GET /v1/diff`) with one path and apply only those hunks through `git apply`, honouring the same `expect` preconditions as whole-path writes. A hunk that no longer exists answers `409 git_state_changed` with `stale_hunks`, and nothing is written unless git accepts the whole patch.
+27 -29
View File
@@ -35,7 +35,6 @@ mod conformance;
mod context_report;
mod core;
mod cost_status;
mod usage_persistence;
/// Guarded installation I/O reusing Engine's confined file primitives.
pub mod delivery_files;
mod doctor;
@@ -46,6 +45,7 @@ mod extension_host;
mod fleet;
mod git_status;
mod keybinding_table;
mod usage_persistence;
use crate::fleet::executor::exec_stream_final_answer_excerpt;
mod hooks;
mod image_attach;
@@ -3104,14 +3104,14 @@ async fn run_async_main_dispatch(
Some(id)
} else if !cli.fresh {
let workspace = resolve_workspace(&cli);
preserve_interrupted_checkpoint_for_explicit_resume(&workspace);
let checkpoint_notice = preserve_interrupted_checkpoint_for_explicit_resume(&workspace);
// Opt-in auto-resume (#2934). Off by default, so the historical
// "plain `codewhale` starts fresh" behaviour is unchanged unless the
// user asked for something else. The decision never resumes an
// archived, unreadable, or foreign-workspace session; every fallback
// carries a receipt rather than silently starting blank.
let (session_id, notice) = resolve_auto_resume(&workspace);
startup_notice = notice;
startup_notice = notice.or(checkpoint_notice);
session_id
} else {
None
@@ -13115,18 +13115,14 @@ fn saved_session_is_newer(
/// the same folder" from re-entering the previous in-flight session while still
/// leaving an explicit resume path.
///
/// Only the newest recent checkpoint drives the notice. The legacy
/// single-slot file is persisted as a regular session and consumed (today's
/// behavior for that slot); per-session checkpoint files are persisted but
/// left in place — they may belong to a live session in another terminal,
/// and `--continue` reads them directly.
fn preserve_interrupted_checkpoint_for_explicit_resume(launch_workspace: &Path) {
let Some(manager) = session_manager::SessionManager::default_location().ok() else {
return;
};
let Some(newest) = load_recent_checkpoints(&manager).into_iter().next() else {
return;
};
/// Only the newest recent checkpoint drives the notice, which is returned for
/// the TUI to show. The legacy single-slot file is migrated to a per-session
/// checkpoint and consumed; per-session checkpoint files are left in place —
/// they may belong to a live session in another terminal, and `--continue`
/// reads them directly.
fn preserve_interrupted_checkpoint_for_explicit_resume(launch_workspace: &Path) -> Option<String> {
let manager = session_manager::SessionManager::default_location().ok()?;
let newest = load_recent_checkpoints(&manager).into_iter().next()?;
let session_workspace = newest.session.metadata.workspace.clone();
// #4479: removed save_session call — checkpoint should not be auto-promoted to session
@@ -13139,20 +13135,22 @@ fn preserve_interrupted_checkpoint_for_explicit_resume(launch_workspace: &Path)
}
let age_str = checkpoint_age_label(newest.age);
if session_manager::workspace_scope_matches(&session_workspace, launch_workspace) {
eprintln!(
"Found an in-flight session snapshot ({age_str}). Starting a new \
session. Run `codewhale --continue` to resume it."
);
} else {
eprintln!(
"Note: an interrupted session from another workspace ({}) is \
available. Run `codewhale sessions` to list saved sessions. Starting \
fresh in {}.",
session_workspace.display(),
launch_workspace.display(),
);
}
Some(
if session_manager::workspace_scope_matches(&session_workspace, launch_workspace) {
format!(
"Found an in-flight session snapshot ({age_str}). Starting a new \
session. Run `codewhale --continue` to resume it."
)
} else {
format!(
"Note: an interrupted session from another workspace ({}) is \
available. Run `codewhale sessions` to list saved sessions. Starting \
fresh in {}.",
session_workspace.display(),
launch_workspace.display(),
)
},
)
}
/// Load project-level config from `$WORKSPACE/.codewhale/config.toml`, with