Skip to content

Commit aeca799

Browse files
nori-sessions[bot]CSResselnori-agent
authored
feat(config): User-defined working messages list (#466)
## Summary 🤖 Generated with [Nori](https://noriagentic.com/) - Adds `[tui].custom_working_message_list` to home Nori config so users can supply their own array of working messages, overriding the builtin whimsical pool while keeping the existing `custom_working_messages` on/off toggle from #464. - Consolidates status-header selection through a single `pick_status_message(bool, &[String])` helper, fixing a regression where `on_task_started` ignored the toggle for any task after the first. - `/config` menu still only edits the boolean; the user list is TOML-only. The menu entry's description now advertises when a custom list is active so toggling does not surprise the user. ## Behavior - `custom_working_messages = false` → plain `Working` (existing). - `custom_working_messages = true` (default), no list → builtin whimsical rotation (existing). - `custom_working_messages = true`, `custom_working_message_list = ["foo", "bar"]` → samples from user list (new). - `custom_working_messages = false`, list set → still plain `Working` (bool wins). ## Test Plan - [x] `cargo test -p codex-core --lib custom_working_message` (5 passed) - [x] `cargo test -p nori-acp --lib test_nori_config` (7 passed) - [x] All 1219 nori-tui lib tests pass; new tests for widget, on_task_started bug fix, and picker description verified GREEN. - [x] `just fix -p codex-core -p nori-acp -p nori-tui` clean. - [x] `cargo fmt`. - [ ] Reviewer: confirm `cargo build --bin nori` and `cargo test -p tui-pty-e2e` pass in CI. Share Nori with your team: https://www.npmjs.com/package/nori-skillsets Co-authored-by: Cliff <clifford@tilework.tech> Co-authored-by: Nori <contact@tilework.tech>
1 parent 0299264 commit aeca799

18 files changed

Lines changed: 262 additions & 24 deletions

File tree

nori-rs/acp/docs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ The `AcpBackendConfig` struct carries both `os_notifications` and `notify_after_
165165

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

168-
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.
168+
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. The companion `custom_working_message_list` accepts an array of strings; when non-empty and `custom_working_messages` is `true`, the TUI samples from the user's list instead of the builtin whimsical messages. Both values are resolved onto `NoriConfig` in `loader.rs` and mirrored through `codex-core`'s config. The `/config` menu only toggles the boolean; the user list is TOML-only and the menu's "Custom Working Messages" entry advertises when a custom list is active.
169169

170170

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

nori-rs/acp/src/config/loader.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ impl NoriConfig {
169169
file_manager: toml.tui.file_manager,
170170
pinned_plan_drawer: toml.tui.pinned_plan_drawer.unwrap_or(false),
171171
custom_working_messages: toml.tui.custom_working_messages.unwrap_or(true),
172+
custom_working_message_list: toml
173+
.tui
174+
.custom_working_message_list
175+
.clone()
176+
.unwrap_or_default(),
172177
auto_worktree,
173178
footer_segment_config: super::types::FooterSegmentConfig::from_toml(
174179
&toml.tui.footer_segments,

nori-rs/acp/src/config/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ terminal_notifications = "disabled"
126126
os_notifications = "disabled"
127127
vertical_footer = true
128128
custom_working_messages = false
129+
custom_working_message_list = ["alpha", "beta"]
129130
"#;
130131
let config: NoriConfigToml = toml::from_str(toml_str).unwrap();
131132

@@ -143,6 +144,10 @@ custom_working_messages = false
143144
assert_eq!(config.tui.os_notifications, Some(OsNotifications::Disabled));
144145
assert_eq!(config.tui.vertical_footer, Some(true));
145146
assert_eq!(config.tui.custom_working_messages, Some(false));
147+
assert_eq!(
148+
config.tui.custom_working_message_list,
149+
Some(vec!["alpha".to_string(), "beta".to_string()])
150+
);
146151
}
147152

148153
#[test]
@@ -159,6 +164,7 @@ model = "gemini"
159164
animations = false
160165
vertical_footer = true
161166
custom_working_messages = false
167+
custom_working_message_list = ["alpha", "beta"]
162168
"#,
163169
)
164170
.unwrap();
@@ -174,6 +180,10 @@ custom_working_messages = false
174180
assert_eq!(config.os_notifications, OsNotifications::Enabled); // default
175181
assert!(config.vertical_footer);
176182
assert!(!config.custom_working_messages);
183+
assert_eq!(
184+
config.custom_working_message_list,
185+
vec!["alpha".to_string(), "beta".to_string()]
186+
);
177187
}
178188

179189
#[test]

nori-rs/acp/src/config/types/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,6 +1388,11 @@ pub struct TuiConfigToml {
13881388

13891389
/// Show rotating custom messages while the agent is working.
13901390
pub custom_working_messages: Option<bool>,
1391+
1392+
/// User-supplied list of working messages. When non-empty and
1393+
/// `custom_working_messages` is enabled, the TUI samples from this list
1394+
/// instead of the builtin whimsical messages.
1395+
pub custom_working_message_list: Option<Vec<String>>,
13911396
}
13921397

13931398
/// Resolved TUI configuration
@@ -1567,6 +1572,11 @@ pub struct NoriConfig {
15671572
/// Show rotating custom messages while the agent is working.
15681573
pub custom_working_messages: bool,
15691574

1575+
/// Optional user-supplied list of working messages. When non-empty and
1576+
/// `custom_working_messages` is `true`, the TUI samples from this list
1577+
/// instead of the builtin whimsical messages.
1578+
pub custom_working_message_list: Vec<String>,
1579+
15701580
/// Footer segment visibility configuration.
15711581
pub footer_segment_config: FooterSegmentConfig,
15721582

@@ -1660,6 +1670,7 @@ impl Default for NoriConfig {
16601670
file_manager: None,
16611671
pinned_plan_drawer: false,
16621672
custom_working_messages: true,
1673+
custom_working_message_list: Vec::new(),
16631674
footer_segment_config: FooterSegmentConfig::default(),
16641675
nori_home: PathBuf::from(".nori/cli"),
16651676
cwd: std::env::current_dir().unwrap_or_default(),

nori-rs/core/docs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Notification modes:
115115
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`).
116116
2. **External script** (`notify_command` configured): Invokes user-specified command with JSON payload.
117117

118-
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.
118+
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. Core additionally carries `Config::custom_working_message_list: Vec<String>`, mirroring `[tui].custom_working_message_list`; when non-empty and `custom_working_messages` is `true`, the TUI samples this user list instead of the builtin whimsical messages.
119119

120120
### Things to Know
121121

nori-rs/core/src/config/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ pub struct Config {
157157
/// Show rotating custom messages while the agent is working.
158158
pub custom_working_messages: bool,
159159

160+
/// Optional user-supplied working messages that override the builtin list
161+
/// when `custom_working_messages` is enabled. Empty means "use builtin".
162+
pub custom_working_message_list: Vec<String>,
163+
160164
/// The directory that should be treated as the current working directory
161165
/// for the session. All relative paths inside the business-logic layer are
162166
/// resolved against this path.
@@ -1269,6 +1273,11 @@ impl Config {
12691273
.as_ref()
12701274
.map(|t| t.custom_working_messages)
12711275
.unwrap_or(true),
1276+
custom_working_message_list: cfg
1277+
.tui
1278+
.as_ref()
1279+
.map(|t| t.custom_working_message_list.clone())
1280+
.unwrap_or_default(),
12721281
otel: {
12731282
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
12741283
let log_user_prompt = t.log_user_prompt.unwrap_or(false);

nori-rs/core/src/config/tests/part1.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,49 @@ custom_working_messages = false
7373
assert!(!tui.custom_working_messages);
7474
}
7575

76+
#[test]
77+
fn tui_config_custom_working_message_list_defaults_to_empty() {
78+
let cfg = r#"
79+
[tui]
80+
"#;
81+
82+
let parsed = toml::from_str::<ConfigToml>(cfg)
83+
.expect("TUI config without custom_working_message_list should succeed");
84+
let tui = parsed.tui.expect("config should include tui section");
85+
86+
assert!(tui.custom_working_message_list.is_empty());
87+
}
88+
89+
#[test]
90+
fn tui_config_custom_working_message_list_accepts_strings() {
91+
let cfg = r#"
92+
[tui]
93+
custom_working_message_list = ["Compiling thoughts", "Reticulating splines"]
94+
"#;
95+
96+
let parsed = toml::from_str::<ConfigToml>(cfg)
97+
.expect("TUI config with custom_working_message_list should succeed");
98+
let tui = parsed.tui.expect("config should include tui section");
99+
100+
assert_eq!(
101+
tui.custom_working_message_list,
102+
vec![
103+
"Compiling thoughts".to_string(),
104+
"Reticulating splines".to_string(),
105+
]
106+
);
107+
}
108+
109+
#[test]
110+
fn tui_config_custom_working_message_list_rejects_non_strings() {
111+
let cfg = r#"
112+
[tui]
113+
custom_working_message_list = [42]
114+
"#;
115+
116+
assert!(toml::from_str::<ConfigToml>(cfg).is_err());
117+
}
118+
76119
#[test]
77120
fn test_sandbox_config_parsing() {
78121
let sandbox_full_access = r#"

nori-rs/core/src/config/tests/part3.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
548548
tui_notifications: true,
549549
animations: true,
550550
custom_working_messages: true,
551+
custom_working_message_list: Vec::new(),
551552
otel: OtelConfig::default(),
552553
acp_allow_http_fallback: false,
553554
},
@@ -622,6 +623,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
622623
tui_notifications: true,
623624
animations: true,
624625
custom_working_messages: true,
626+
custom_working_message_list: Vec::new(),
625627
otel: OtelConfig::default(),
626628
acp_allow_http_fallback: false,
627629
};

nori-rs/core/src/config/tests/part4.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
6767
tui_notifications: true,
6868
animations: true,
6969
custom_working_messages: true,
70+
custom_working_message_list: Vec::new(),
7071
otel: OtelConfig::default(),
7172
acp_allow_http_fallback: false,
7273
};
@@ -142,6 +143,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
142143
tui_notifications: true,
143144
animations: true,
144145
custom_working_messages: true,
146+
custom_working_message_list: Vec::new(),
145147
otel: OtelConfig::default(),
146148
acp_allow_http_fallback: false,
147149
};

nori-rs/core/src/config/types.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@ pub struct Tui {
375375
/// Defaults to `true`.
376376
#[serde(default = "default_true")]
377377
pub custom_working_messages: bool,
378+
379+
/// User-supplied list of working messages. When non-empty and
380+
/// `custom_working_messages` is `true`, the TUI samples from this list
381+
/// instead of the builtin whimsical messages. Defaults to empty.
382+
#[serde(default)]
383+
pub custom_working_message_list: Vec<String>,
378384
}
379385

380386
const fn default_true() -> bool {

0 commit comments

Comments
 (0)