From 4a4d6ba4fab07bc3f8cc24a0a37df2a62570e1cc Mon Sep 17 00:00:00 2001 From: Clifford Ressel Date: Tue, 5 May 2026 15:45:35 -0400 Subject: [PATCH 1/2] fix(cli): show resume hint without token usage --- nori-rs/cli/docs.md | 2 +- nori-rs/cli/src/main.rs | 52 ++++++++++++++++--- nori-rs/tui-pty-e2e/src/lib.rs | 5 ++ .../exit_statistics__exit_message_ctrl_d.snap | 1 + ...t_statistics__exit_message_slash_exit.snap | 1 + nori-rs/tui/src/app/mod.rs | 3 ++ nori-rs/tui/src/lib.rs | 4 ++ 7 files changed, 59 insertions(+), 9 deletions(-) diff --git a/nori-rs/cli/docs.md b/nori-rs/cli/docs.md index c8e1398d8..de019ffa7 100644 --- a/nori-rs/cli/docs.md +++ b/nori-rs/cli/docs.md @@ -130,6 +130,6 @@ On non-Windows, `wsl_paths.rs` normalizes paths for WSL environments to ensure c **Exit Handling:** -`handle_app_exit()` prints token usage and session resume hints after TUI exits, then optionally runs update actions if the user requested an upgrade. +`handle_app_exit()` prints token usage when available and prints a copyable `nori resume ` hint for sessions that recorded activity, then optionally runs update actions if the user requested an upgrade. Created and maintained by Nori. diff --git a/nori-rs/cli/src/main.rs b/nori-rs/cli/src/main.rs index d2755beae..95e26399f 100644 --- a/nori-rs/cli/src/main.rs +++ b/nori-rs/cli/src/main.rs @@ -216,19 +216,19 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec String { line = result; } + // Resume hint: "nori resume 019bb411-..." -> "nori resume [SESSION_ID]" + if let Some(result) = replace_after_marker(&line, "nori resume", "[SESSION_ID]") { + line = result; + } + // Git diff stats in footer: strip "· +N -M " segment // The stats vary based on repo state and would cause flaky snapshots line = strip_git_stats_segment(&line); diff --git a/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_ctrl_d.snap b/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_ctrl_d.snap index c47565585..14e5462a6 100644 --- a/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_ctrl_d.snap +++ b/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_ctrl_d.snap @@ -20,3 +20,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) │ Subagents Used │ │ (none) │ ╰───────────────────────────────────────╯ +To continue this session, run nori resume [SESSION_ID] diff --git a/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_slash_exit.snap b/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_slash_exit.snap index c47565585..14e5462a6 100644 --- a/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_slash_exit.snap +++ b/nori-rs/tui-pty-e2e/tests/snapshots/exit_statistics__exit_message_slash_exit.snap @@ -20,3 +20,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) │ Subagents Used │ │ (none) │ ╰───────────────────────────────────────╯ +To continue this session, run nori resume [SESSION_ID] diff --git a/nori-rs/tui/src/app/mod.rs b/nori-rs/tui/src/app/mod.rs index 2c22686ad..47a139903 100644 --- a/nori-rs/tui/src/app/mod.rs +++ b/nori-rs/tui/src/app/mod.rs @@ -69,6 +69,7 @@ const GPT_5_1_CODEX_MIGRATION_AUTH_MODES: [AuthMode; 1] = [AuthMode::ChatGPT]; pub struct AppExitInfo { pub token_usage: TokenUsage, pub conversation_id: Option, + pub conversation_has_activity: bool, pub update_action: Option, } @@ -187,6 +188,7 @@ async fn handle_model_migration_prompt_if_needed( return Some(AppExitInfo { token_usage: TokenUsage::default(), conversation_id: None, + conversation_has_activity: false, update_action: None, }); } @@ -481,6 +483,7 @@ impl App { Ok(AppExitInfo { token_usage: app.token_usage(), conversation_id: app.chat_widget.conversation_id(), + conversation_has_activity: app.chat_widget.session_stats().has_activity(), update_action: app.pending_update_action, }) } diff --git a/nori-rs/tui/src/lib.rs b/nori-rs/tui/src/lib.rs index e8e9649d9..462be05f6 100644 --- a/nori-rs/tui/src/lib.rs +++ b/nori-rs/tui/src/lib.rs @@ -417,6 +417,7 @@ async fn run_ratatui_app( return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), conversation_id: None, + conversation_has_activity: false, update_action: Some(action), }); } @@ -456,6 +457,7 @@ async fn run_ratatui_app( return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), conversation_id: None, + conversation_has_activity: false, update_action: None, }); } @@ -567,6 +569,7 @@ async fn run_ratatui_app( return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), conversation_id: None, + conversation_has_activity: false, update_action: None, }); } @@ -637,6 +640,7 @@ fn resume_startup_error(tui: &mut Tui, message: String) -> color_eyre::Result Date: Tue, 5 May 2026 16:44:56 -0400 Subject: [PATCH 2/2] fix(cli): put resume command on its own line --- nori-rs/cli/docs.md | 2 +- nori-rs/cli/src/main.rs | 20 ++++++++------ .../exit_statistics__exit_message_ctrl_d.snap | 3 ++- ...t_statistics__exit_message_slash_exit.snap | 3 ++- nori-rs/tui/docs.md | 2 ++ nori-rs/tui/src/app/event_handling.rs | 10 ++++--- nori-rs/tui/src/app/mod.rs | 18 +++++++++---- nori-rs/tui/src/app/tests.rs | 26 ++++++++++++++++--- nori-rs/tui/src/lib.rs | 2 ++ 9 files changed, 64 insertions(+), 22 deletions(-) diff --git a/nori-rs/cli/docs.md b/nori-rs/cli/docs.md index de019ffa7..d26c1c940 100644 --- a/nori-rs/cli/docs.md +++ b/nori-rs/cli/docs.md @@ -130,6 +130,6 @@ On non-Windows, `wsl_paths.rs` normalizes paths for WSL environments to ensure c **Exit Handling:** -`handle_app_exit()` prints token usage when available and prints a copyable `nori resume ` hint for sessions that recorded activity, then optionally runs update actions if the user requested an upgrade. +`handle_app_exit()` prints token usage when available and prints a copyable two-line resume hint for sessions that recorded activity. The lead line ends with `run:` and the next line contains only `nori resume ` so the command can be copied without surrounding output. It then optionally runs update actions if the user requested an upgrade. Created and maintained by Nori. diff --git a/nori-rs/cli/src/main.rs b/nori-rs/cli/src/main.rs index 95e26399f..fce82f5b8 100644 --- a/nori-rs/cli/src/main.rs +++ b/nori-rs/cli/src/main.rs @@ -23,6 +23,8 @@ use nori_cli::login::run_logout; use nori_tui::AppExitInfo; use nori_tui::Cli as TuiCli; +use nori_tui::RESUME_HINT_LEAD; +use nori_tui::resume_command_for_conversation; use nori_tui::update_action::UpdateAction; use owo_colors::OwoColorize; use std::path::PathBuf; @@ -229,13 +231,14 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec` command on its own line after the `run:` lead text. + **Session Resume (`/resume`):** The `/resume` command allows reconnecting to a previous ACP session. It uses the ACP agent's `session/load` RPC when available, and otherwise falls back to a fresh ACP session plus normalized replay derived from the saved transcript (see `@/nori-rs/acp/docs.md`). diff --git a/nori-rs/tui/src/app/event_handling.rs b/nori-rs/tui/src/app/event_handling.rs index 936e5d07b..0b9385d98 100644 --- a/nori-rs/tui/src/app/event_handling.rs +++ b/nori-rs/tui/src/app/event_handling.rs @@ -80,6 +80,7 @@ impl App { let summary = session_summary( self.chat_widget.token_usage(), self.chat_widget.conversation_id(), + self.chat_widget.session_stats().has_activity(), ); self.shutdown_current_conversation(); let init = self.chat_widget_init( @@ -93,10 +94,13 @@ impl App { self.chat_widget = ChatWidget::new(init); self.configure_new_chat_widget(); if let Some(summary) = summary { - let mut lines: Vec> = vec![summary.usage_line.clone().into()]; + let mut lines: Vec> = Vec::new(); + if let Some(usage_line) = summary.usage_line { + lines.push(usage_line.into()); + } if let Some(command) = summary.resume_command { - let spans = vec!["To continue this session, run ".into(), command.cyan()]; - lines.push(spans.into()); + lines.push(RESUME_HINT_LEAD.into()); + lines.push(command.cyan().into()); } self.chat_widget.add_plain_history_lines(lines); } diff --git a/nori-rs/tui/src/app/mod.rs b/nori-rs/tui/src/app/mod.rs index 47a139903..74eac0635 100644 --- a/nori-rs/tui/src/app/mod.rs +++ b/nori-rs/tui/src/app/mod.rs @@ -64,6 +64,7 @@ use crate::history_cell::UpdateAvailableHistoryCell; const GPT_5_1_MIGRATION_AUTH_MODES: [AuthMode; 2] = [AuthMode::ChatGPT, AuthMode::ApiKey]; const GPT_5_1_CODEX_MIGRATION_AUTH_MODES: [AuthMode; 1] = [AuthMode::ChatGPT]; +pub const RESUME_HINT_LEAD: &str = "To continue this session, run:"; #[derive(Debug, Clone)] pub struct AppExitInfo { @@ -76,23 +77,30 @@ pub struct AppExitInfo { fn session_summary( token_usage: TokenUsage, conversation_id: Option, + conversation_has_activity: bool, ) -> Option { - if token_usage.is_zero() { + let usage_line = (!token_usage.is_zero()).then(|| FinalOutput::from(token_usage).to_string()); + let resume_command = conversation_id + .filter(|_| conversation_has_activity) + .map(|conversation_id| resume_command_for_conversation(&conversation_id)); + + if usage_line.is_none() && resume_command.is_none() { return None; } - let usage_line = FinalOutput::from(token_usage).to_string(); - let resume_command = - conversation_id.map(|conversation_id| format!("nori resume {conversation_id}")); Some(SessionSummary { usage_line, resume_command, }) } +pub fn resume_command_for_conversation(conversation_id: &ConversationId) -> String { + format!("nori resume {conversation_id}") +} + #[derive(Debug, Clone, PartialEq, Eq)] struct SessionSummary { - usage_line: String, + usage_line: Option, resume_command: Option, } diff --git a/nori-rs/tui/src/app/tests.rs b/nori-rs/tui/src/app/tests.rs index 61ee1031d..1f0faa9d3 100644 --- a/nori-rs/tui/src/app/tests.rs +++ b/nori-rs/tui/src/app/tests.rs @@ -391,7 +391,7 @@ fn apply_approval_preset_updates_app_widget_and_backend_for_full_access() { #[test] fn session_summary_skip_zero_usage() { - assert!(session_summary(TokenUsage::default(), None).is_none()); + assert!(session_summary(TokenUsage::default(), None, false).is_none()); } #[test] @@ -404,10 +404,10 @@ fn session_summary_includes_resume_hint() { }; let conversation = ConversationId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); - let summary = session_summary(usage, Some(conversation)).expect("summary"); + let summary = session_summary(usage, Some(conversation), true).expect("summary"); assert_eq!( summary.usage_line, - "Token usage: total=12 input=10 output=2" + Some("Token usage: total=12 input=10 output=2".to_string()) ); assert_eq!( summary.resume_command, @@ -415,6 +415,26 @@ fn session_summary_includes_resume_hint() { ); } +#[test] +fn session_summary_includes_resume_hint_without_token_usage() { + let conversation = ConversationId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); + + let summary = + session_summary(TokenUsage::default(), Some(conversation), true).expect("summary"); + assert_eq!(summary.usage_line, None); + assert_eq!( + summary.resume_command, + Some("nori resume 123e4567-e89b-12d3-a456-426614174000".to_string()) + ); +} + +#[test] +fn session_summary_skips_resume_hint_without_activity() { + let conversation = ConversationId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); + + assert!(session_summary(TokenUsage::default(), Some(conversation), false).is_none()); +} + #[test] fn gpt5_migration_allows_api_key_and_chatgpt() { assert!(migration_prompt_allows_auth_mode( diff --git a/nori-rs/tui/src/lib.rs b/nori-rs/tui/src/lib.rs index 462be05f6..18f156f95 100644 --- a/nori-rs/tui/src/lib.rs +++ b/nori-rs/tui/src/lib.rs @@ -6,6 +6,8 @@ use additional_dirs::add_dir_warning_message; use app::App; pub use app::AppExitInfo; +pub use app::RESUME_HINT_LEAD; +pub use app::resume_command_for_conversation; use codex_app_server_protocol::AuthMode; use codex_core::AuthManager; use codex_core::CodexAuth;