From 5b5525edfc9e4f8e07b54fb39be60e72f64212bf Mon Sep 17 00:00:00 2001 From: Clifford Ressel Date: Mon, 18 May 2026 15:54:53 -0400 Subject: [PATCH] feat(tui): render initial config-options banner at session startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first SessionConfigUpdate the agent emits at session boot used to produce no history (it only seeded the diff baseline). Render an explicit banner so the user can see what they're starting with and how to change it: • Claude Code options: Mode=Default, Model=Opus 4.6, Effort=High (/config to change) Values stay cyan+bold for consistency with the existing change-diff cell; the trailing /config hint is dim. Subsequent updates continue to render only changed values, unchanged. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori --- nori-rs/tui/src/chatwidget/helpers.rs | 7 +++ nori-rs/tui/src/chatwidget/tests/part3.rs | 48 +++++++++++++++++-- ..._session_config_update_startup_banner.snap | 5 ++ .../tui/src/nori/session_config_history.rs | 27 +++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 nori-rs/tui/src/chatwidget/tests/snapshots/nori_tui__chatwidget__tests__part3__session_config_update_startup_banner.snap diff --git a/nori-rs/tui/src/chatwidget/helpers.rs b/nori-rs/tui/src/chatwidget/helpers.rs index 1ef9d3ac..e064bb80 100644 --- a/nori-rs/tui/src/chatwidget/helpers.rs +++ b/nori-rs/tui/src/chatwidget/helpers.rs @@ -75,6 +75,13 @@ impl ChatWidget { ), ); } + } else if !next_snapshot.is_empty() { + self.add_to_history( + crate::nori::session_config_history::new_agent_options_initial_history_cell( + self.bottom_pane.agent_display_name(), + config_options, + ), + ); } self.acp_config_option_snapshot = Some(next_snapshot); diff --git a/nori-rs/tui/src/chatwidget/tests/part3.rs b/nori-rs/tui/src/chatwidget/tests/part3.rs index c41c90c4..63d3cfe6 100644 --- a/nori-rs/tui/src/chatwidget/tests/part3.rs +++ b/nori-rs/tui/src/chatwidget/tests/part3.rs @@ -469,10 +469,10 @@ fn session_config_update_history_shows_only_changed_values_after_baseline() { ], }, )); - assert!( - drain_insert_history(&mut rx).is_empty(), - "the first config snapshot should establish a baseline without history noise" - ); + // Drain (and discard) the initial-snapshot banner so the snapshot below + // captures only the subsequent change-diff cell. The banner is covered + // by `session_config_update_history_renders_startup_banner_on_first_snapshot`. + let _ = drain_insert_history(&mut rx); chat.handle_client_event(nori_protocol::ClientEvent::SessionConfigUpdate( nori_protocol::SessionConfigUpdate { @@ -509,6 +509,46 @@ fn session_config_update_history_shows_only_changed_values_after_baseline() { assert_snapshot!("session_config_update_changed_values_history", rendered); } +#[test] +fn session_config_update_history_renders_startup_banner_on_first_snapshot() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + chat.set_agent("claude-code"); + + chat.handle_client_event(nori_protocol::ClientEvent::SessionConfigUpdate( + nori_protocol::SessionConfigUpdate { + config_options: vec![ + select_config_option( + "mode", + "Mode", + "default", + &[("default", "Default"), ("plan", "Plan")], + ), + select_config_option( + "model", + "Model", + "opus-4-6", + &[("opus-4-6", "Opus 4.6"), ("sonnet-4-6", "Sonnet 4.6")], + ), + select_config_option( + "effort", + "Effort", + "medium", + &[("medium", "Medium"), ("high", "High")], + ), + ], + }, + )); + + let cells = drain_insert_history(&mut rx); + let rendered = cells + .iter() + .map(|lines| lines_to_single_string(lines)) + .collect::>() + .join("\n"); + + assert_snapshot!("session_config_update_startup_banner", rendered); +} + #[test] fn session_config_set_history_uses_final_agent_named_message() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); diff --git a/nori-rs/tui/src/chatwidget/tests/snapshots/nori_tui__chatwidget__tests__part3__session_config_update_startup_banner.snap b/nori-rs/tui/src/chatwidget/tests/snapshots/nori_tui__chatwidget__tests__part3__session_config_update_startup_banner.snap new file mode 100644 index 00000000..a2a8a69b --- /dev/null +++ b/nori-rs/tui/src/chatwidget/tests/snapshots/nori_tui__chatwidget__tests__part3__session_config_update_startup_banner.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/chatwidget/tests/part3.rs +expression: rendered +--- +• Claude Code options: Mode=Default, Model=Opus 4.6, Effort=Medium (/config to change) diff --git a/nori-rs/tui/src/nori/session_config_history.rs b/nori-rs/tui/src/nori/session_config_history.rs index e76bdf00..20da90d8 100644 --- a/nori-rs/tui/src/nori/session_config_history.rs +++ b/nori-rs/tui/src/nori/session_config_history.rs @@ -39,6 +39,33 @@ pub(crate) fn changed_values( .collect() } +/// Initial-snapshot banner shown the first time the agent announces its +/// session config options. Lists every option's current value and surfaces +/// the `/config` affordance so the user knows how to change them. +pub(crate) fn new_agent_options_initial_history_cell( + agent_display_name: &str, + config_options: &[acp::SessionConfigOption], +) -> PlainHistoryCell { + let agent_display_name = if agent_display_name.is_empty() { + "Agent" + } else { + agent_display_name + }; + let values: Vec = + config_options.iter().filter_map(display_value).collect(); + + let mut line = vec!["• ".dim(), format!("{agent_display_name} options: ").into()]; + for (index, value) in values.iter().enumerate() { + if index > 0 { + line.push(", ".into()); + } + line.push(format!("{}={}", value.name, value.value).cyan().bold()); + } + line.push(" (/config to change)".dim()); + + PlainHistoryCell::new(vec![Line::from(line)]) +} + pub(crate) fn new_agent_options_history_cell( agent_display_name: &str, changes: &[SessionConfigDisplayValue],