Skip to content
Draft
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
4 changes: 4 additions & 0 deletions nori-rs/acp/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ Three config enums control notification behavior, all stored in the `[tui]` sect

The `AcpBackendConfig` struct carries both `os_notifications` and `notify_after_idle` so the backend can configure the `UserNotifier` and the idle timer respectively. Terminal notifications flow separately through `codex-core`'s `Config::tui_notifications` bool to the TUI's `ChatWidget::notify()` method.

**TUI Display Configuration** (`config/types/mod.rs`):

The `[tui]` section also owns display-only preferences consumed by `@/nori-rs/tui/`. `custom_working_messages` defaults to `true`; setting it to `false` disables the rotating whimsical status header list and lets the TUI use a plain "Working" label while a task starts. This value is resolved onto `NoriConfig` in `loader.rs`, mirrored through `codex-core`'s config, and can be changed from the `/config` menu.


**Hotkey Configuration** (`config/types/mod.rs`):

Expand Down
1 change: 1 addition & 0 deletions nori-rs/acp/src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ impl NoriConfig {
skillset_per_session,
file_manager: toml.tui.file_manager,
pinned_plan_drawer: toml.tui.pinned_plan_drawer.unwrap_or(false),
custom_working_messages: toml.tui.custom_working_messages.unwrap_or(true),
auto_worktree,
footer_segment_config: super::types::FooterSegmentConfig::from_toml(
&toml.tui.footer_segments,
Expand Down
4 changes: 4 additions & 0 deletions nori-rs/acp/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ animations = false
terminal_notifications = "disabled"
os_notifications = "disabled"
vertical_footer = true
custom_working_messages = false
"#;
let config: NoriConfigToml = toml::from_str(toml_str).unwrap();

Expand All @@ -141,6 +142,7 @@ vertical_footer = true
);
assert_eq!(config.tui.os_notifications, Some(OsNotifications::Disabled));
assert_eq!(config.tui.vertical_footer, Some(true));
assert_eq!(config.tui.custom_working_messages, Some(false));
}

#[test]
Expand All @@ -156,6 +158,7 @@ model = "gemini"
[tui]
animations = false
vertical_footer = true
custom_working_messages = false
"#,
)
.unwrap();
Expand All @@ -170,6 +173,7 @@ vertical_footer = true
); // default
assert_eq!(config.os_notifications, OsNotifications::Enabled); // default
assert!(config.vertical_footer);
assert!(!config.custom_working_messages);
}

#[test]
Expand Down
7 changes: 7 additions & 0 deletions nori-rs/acp/src/config/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,9 @@ pub struct TuiConfigToml {

/// Pin plan updates to a drawer in the viewport instead of history cells.
pub pinned_plan_drawer: Option<bool>,

/// Show rotating custom messages while the agent is working.
pub custom_working_messages: Option<bool>,
}

/// Resolved TUI configuration
Expand Down Expand Up @@ -1561,6 +1564,9 @@ pub struct NoriConfig {
/// Pin plan updates to a drawer in the viewport instead of history cells.
pub pinned_plan_drawer: bool,

/// Show rotating custom messages while the agent is working.
pub custom_working_messages: bool,

/// Footer segment visibility configuration.
pub footer_segment_config: FooterSegmentConfig,

Expand Down Expand Up @@ -1653,6 +1659,7 @@ impl Default for NoriConfig {
skillset_per_session: false,
file_manager: None,
pinned_plan_drawer: false,
custom_working_messages: true,
footer_segment_config: FooterSegmentConfig::default(),
nori_home: PathBuf::from(".nori/cli"),
cwd: std::env::current_dir().unwrap_or_default(),
Expand Down
2 changes: 1 addition & 1 deletion nori-rs/core/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Notification modes:
1. **Native notifications** (`use_native: true`): Uses `notify-rust` for desktop notifications. All calls to `send_native()` are non-blocking -- they spawn a background thread to call `notif.show()`, because some platforms (notably macOS) block synchronously on that call. On X11 Linux, the spawned thread also handles click-to-focus via `wmctrl` or `xdotool`. The `use_native` flag is controlled by `OsNotifications` in the ACP config layer (`@/nori-rs/acp/src/config/types.rs`).
2. **External script** (`notify_command` configured): Invokes user-specified command with JSON payload.

Core's `Config::tui_notifications` is a simple `bool` that controls whether the TUI sends OSC 9 terminal escape sequence notifications. It derives its value from the ACP config's `TerminalNotifications` enum during config loading.
Core's `Config::tui_notifications` is a simple `bool` that controls whether the TUI sends OSC 9 terminal escape sequence notifications. It derives its value from the ACP config's `TerminalNotifications` enum during config loading. Core also carries TUI display booleans such as `animations` and `custom_working_messages`; the latter mirrors `[tui].custom_working_messages` from Nori config so the TUI can choose between rotating custom working headers and the plain `Working` label without re-reading config.

### Things to Know

Expand Down
8 changes: 8 additions & 0 deletions nori-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ pub struct Config {
/// Enable ASCII animations and shimmer effects in the TUI.
pub animations: bool,

/// Show rotating custom messages while the agent is working.
pub custom_working_messages: bool,

/// The directory that should be treated as the current working directory
/// for the session. All relative paths inside the business-logic layer are
/// resolved against this path.
Expand Down Expand Up @@ -1261,6 +1264,11 @@ impl Config {
.map(|t| t.terminal_notifications)
.unwrap_or(true),
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
custom_working_messages: cfg
.tui
.as_ref()
.map(|t| t.custom_working_messages)
.unwrap_or(true),
otel: {
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
Expand Down
27 changes: 27 additions & 0 deletions nori-rs/core/src/config/tests/part1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,33 @@ fn tui_config_missing_terminal_notifications_field_defaults_to_true() {
assert!(tui.terminal_notifications);
}

#[test]
fn tui_config_custom_working_messages_defaults_to_true() {
let cfg = r#"
[tui]
"#;

let parsed = toml::from_str::<ConfigToml>(cfg)
.expect("TUI config without custom_working_messages should succeed");
let tui = parsed.tui.expect("config should include tui section");

assert!(tui.custom_working_messages);
}

#[test]
fn tui_config_custom_working_messages_can_be_disabled() {
let cfg = r#"
[tui]
custom_working_messages = false
"#;

let parsed = toml::from_str::<ConfigToml>(cfg)
.expect("TUI config with custom_working_messages disabled should succeed");
let tui = parsed.tui.expect("config should include tui section");

assert!(!tui.custom_working_messages);
}

#[test]
fn test_sandbox_config_parsing() {
let sandbox_full_access = r#"
Expand Down
2 changes: 2 additions & 0 deletions nori-rs/core/src/config/tests/part3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
disable_paste_burst: false,
tui_notifications: true,
animations: true,
custom_working_messages: true,
otel: OtelConfig::default(),
acp_allow_http_fallback: false,
},
Expand Down Expand Up @@ -620,6 +621,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
disable_paste_burst: false,
tui_notifications: true,
animations: true,
custom_working_messages: true,
otel: OtelConfig::default(),
acp_allow_http_fallback: false,
};
Expand Down
2 changes: 2 additions & 0 deletions nori-rs/core/src/config/tests/part4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
disable_paste_burst: false,
tui_notifications: true,
animations: true,
custom_working_messages: true,
otel: OtelConfig::default(),
acp_allow_http_fallback: false,
};
Expand Down Expand Up @@ -140,6 +141,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
disable_paste_burst: false,
tui_notifications: true,
animations: true,
custom_working_messages: true,
otel: OtelConfig::default(),
acp_allow_http_fallback: false,
};
Expand Down
5 changes: 5 additions & 0 deletions nori-rs/core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ pub struct Tui {
/// Defaults to `true`.
#[serde(default = "default_true")]
pub animations: bool,

/// Show rotating custom messages while the agent is working.
/// Defaults to `true`.
#[serde(default = "default_true")]
pub custom_working_messages: bool,
}

const fn default_true() -> bool {
Expand Down
4 changes: 2 additions & 2 deletions nori-rs/tui/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ During background system info collection on unix, `check_worktree_cleanup()` run
| `/agent` | Switch between available ACP agents (dynamically shows current agent name) |
| `/model` | Choose model (dynamically shows current agent/model name) |
| `/approvals` | Choose what Nori can do without approval (dynamically shows current approval mode) |
| `/config` | Toggle TUI settings (pinned plan drawer, vertical footer, terminal notifications, OS notifications, vim mode with enter behavior sub-picker, auto worktree, per session skillsets, notify after idle, hotkeys, script timeout, loop count, footer segments, file manager) |
| `/config` | Toggle TUI settings (pinned plan drawer, custom working messages, vertical footer, terminal notifications, OS notifications, vim mode with enter behavior sub-picker, auto worktree, per session skillsets, notify after idle, hotkeys, script timeout, loop count, footer segments, file manager) |
| `/browse` | Open a terminal file manager to browse and edit files |
| `/new` | Start a new chat during a conversation |
| `/resume` | Resume a previous ACP session |
Expand Down Expand Up @@ -811,7 +811,7 @@ When the user selects an agent (or resumes a session), the TUI shows a "Connecti

**Status Indicator Whimsical Messages (`status_indicator_widget.rs`):**

When the agent begins processing a task, the `StatusIndicatorWidget` displays an animated header with a randomly selected tongue-in-cheek message (e.g., "Thinking really hard", "Hallucinating responsibly") drawn from the `WHIMSICAL_STATUS_MESSAGES` pool via `random_status_message()`. A new random message is selected each time `on_task_started()` fires in `chatwidget/event_handlers.rs`. During streaming, reasoning chunk headers (extracted from bold markdown text) dynamically replace this initial message via `update_status_header()`.
When the agent begins processing a task, the `StatusIndicatorWidget` displays an animated header. By default it chooses a randomly selected tongue-in-cheek message (e.g., "Thinking really hard", "Hallucinating responsibly") drawn from the `WHIMSICAL_STATUS_MESSAGES` pool via `initial_status_message(true)`. Users can opt out with `[tui].custom_working_messages = false` or the `/config` toggle, which makes the initial header the plain `Working` label instead. During streaming, reasoning chunk headers (extracted from bold markdown text) dynamically replace this initial message via `update_status_header()`.

**Terminal Title Management (`terminal_title.rs`, `chatwidget/helpers.rs`):**

Expand Down
21 changes: 21 additions & 0 deletions nori-rs/tui/src/app/config_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,27 @@ impl App {
.add_info_message(format!("Pinned plan drawer {status}."), None);
}

#[cfg(feature = "nori-config")]
pub(super) async fn persist_custom_working_messages_setting(&mut self, enabled: bool) {
self.config.custom_working_messages = enabled;
self.chat_widget.set_custom_working_messages(enabled);

if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_path(&["tui", "custom_working_messages"], toml_value(enabled))
.apply()
.await
{
tracing::error!(error = %err, "failed to persist custom_working_messages setting");
self.chat_widget.add_error_message(format!(
"Failed to save custom_working_messages setting: {err}"
));
return;
}
let status = if enabled { "enabled" } else { "disabled" };
self.chat_widget
.add_info_message(format!("Custom working messages {status}."), None);
}

#[cfg(feature = "nori-config")]
pub(super) async fn persist_skillset_per_session_setting(&mut self, enabled: bool) {
let builder = ConfigEditsBuilder::new(&self.config.codex_home)
Expand Down
4 changes: 4 additions & 0 deletions nori-rs/tui/src/app/event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,10 @@ impl App {
self.persist_pinned_plan_drawer_setting(enabled).await;
}
#[cfg(feature = "nori-config")]
AppEvent::SetConfigCustomWorkingMessages(enabled) => {
self.persist_custom_working_messages_setting(enabled).await;
}
#[cfg(feature = "nori-config")]
AppEvent::OpenSkillsetPerSessionWorktreeChoice => {
self.chat_widget.open_skillset_worktree_choice_picker();
}
Expand Down
4 changes: 4 additions & 0 deletions nori-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ pub(crate) enum AppEvent {
#[cfg(feature = "nori-config")]
SetConfigPinnedPlanDrawer(bool),

/// Set the TUI custom working messages config setting.
#[cfg(feature = "nori-config")]
SetConfigCustomWorkingMessages(bool),

/// Open the worktree choice modal when enabling per-session skillsets.
#[cfg(feature = "nori-config")]
OpenSkillsetPerSessionWorktreeChoice,
Expand Down
24 changes: 24 additions & 0 deletions nori-rs/tui/src/bottom_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub(crate) struct BottomPane {
ctrl_c_quit_hint: bool,
esc_backtrack_hint: bool,
animations_enabled: bool,
custom_working_messages: bool,

/// Inline status indicator shown above the composer while a task is running.
status: Option<StatusIndicatorWidget>,
Expand All @@ -90,6 +91,7 @@ pub(crate) struct BottomPaneParams {
pub(crate) placeholder_text: String,
pub(crate) disable_paste_burst: bool,
pub(crate) animations_enabled: bool,
pub(crate) custom_working_messages: bool,
pub(crate) vertical_footer: bool,
pub(crate) footer_segment_config: nori_acp::config::FooterSegmentConfig,
pub(crate) agent_display_name: String,
Expand All @@ -106,6 +108,7 @@ impl BottomPane {
placeholder_text,
disable_paste_burst,
animations_enabled,
custom_working_messages,
vertical_footer,
footer_segment_config,
agent_display_name,
Expand Down Expand Up @@ -146,6 +149,7 @@ impl BottomPane {
queued_user_messages: QueuedUserMessages::new(),
esc_backtrack_hint: false,
animations_enabled,
custom_working_messages,
context_window_percent: None,
agent_display_name,
agent_slug,
Expand Down Expand Up @@ -350,6 +354,7 @@ impl BottomPane {
self.app_event_tx.clone(),
self.frame_requester.clone(),
self.animations_enabled,
self.custom_working_messages,
));
}
if let Some(status) = self.status.as_mut() {
Expand All @@ -376,6 +381,7 @@ impl BottomPane {
self.app_event_tx.clone(),
self.frame_requester.clone(),
self.animations_enabled,
self.custom_working_messages,
));
self.request_redraw();
}
Expand Down Expand Up @@ -422,6 +428,16 @@ impl BottomPane {
self.composer.set_vertical_footer(vertical_footer);
}

pub(crate) fn set_custom_working_messages(&mut self, enabled: bool) {
self.custom_working_messages = enabled;
if let Some(status) = self.status.as_mut() {
status.update_header(crate::status_indicator_widget::initial_status_message(
enabled,
));
self.request_redraw();
}
}

/// Update the hotkey configuration used by the textarea for editing bindings.
pub(crate) fn set_hotkey_config(&mut self, config: nori_acp::config::HotkeyConfig) {
self.composer.set_hotkey_config(config);
Expand Down Expand Up @@ -799,6 +815,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand All @@ -824,6 +841,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down Expand Up @@ -857,6 +875,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down Expand Up @@ -931,6 +950,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down Expand Up @@ -963,6 +983,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down Expand Up @@ -998,6 +1019,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down Expand Up @@ -1029,6 +1051,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down Expand Up @@ -1060,6 +1083,7 @@ mod tests {
placeholder_text: "Ask Nori to do anything".to_string(),
disable_paste_burst: false,
animations_enabled: true,
custom_working_messages: true,
vertical_footer: false,
footer_segment_config: nori_acp::config::FooterSegmentConfig::default(),
agent_display_name: String::new(),
Expand Down
Loading
Loading