mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
2 Commits
shijie/req
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
936ec73ab7 | ||
|
|
984f6f31fb |
@@ -3603,6 +3603,7 @@ mod tests {
|
||||
session_manager: ExecSessionManager::default(),
|
||||
unified_exec_manager: UnifiedExecSessionManager::default(),
|
||||
notify: None,
|
||||
plan_step_notifications: false,
|
||||
rollout: Mutex::new(None),
|
||||
state: Mutex::new(State {
|
||||
history: ConversationHistory::new(),
|
||||
|
||||
@@ -122,6 +122,9 @@ pub struct Config {
|
||||
/// and turn completions when not focused.
|
||||
pub tui_notifications: Notifications,
|
||||
|
||||
/// Enable plan step completion notifications in the UI and external notifier.
|
||||
pub plan_step_notifications: 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.
|
||||
@@ -1053,6 +1056,11 @@ impl Config {
|
||||
.as_ref()
|
||||
.map(|t| t.notifications.clone())
|
||||
.unwrap_or_default(),
|
||||
plan_step_notifications: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|t| t.plan_step_notifications)
|
||||
.unwrap_or(true),
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
@@ -1617,6 +1625,7 @@ model_verbosity = "high"
|
||||
active_profile: Some("o3".to_string()),
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
plan_step_notifications: true,
|
||||
},
|
||||
o3_profile_config
|
||||
);
|
||||
@@ -1675,6 +1684,7 @@ model_verbosity = "high"
|
||||
active_profile: Some("gpt3".to_string()),
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
plan_step_notifications: true,
|
||||
};
|
||||
|
||||
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
|
||||
@@ -1748,6 +1758,7 @@ model_verbosity = "high"
|
||||
active_profile: Some("zdr".to_string()),
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
plan_step_notifications: true,
|
||||
};
|
||||
|
||||
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
|
||||
@@ -1807,6 +1818,7 @@ model_verbosity = "high"
|
||||
active_profile: Some("gpt5".to_string()),
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
plan_step_notifications: true,
|
||||
};
|
||||
|
||||
assert_eq!(expected_gpt5_profile_config, gpt5_profile_config);
|
||||
@@ -1913,12 +1925,19 @@ trust_level = "trusted"
|
||||
|
||||
#[cfg(test)]
|
||||
mod notifications_tests {
|
||||
use crate::config::{Config, ConfigOverrides, ConfigToml};
|
||||
use crate::config_types::Notifications;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
struct TuiTomlTest {
|
||||
notifications: Notifications,
|
||||
#[serde(default = "default_true")]
|
||||
plan_step_notifications: bool,
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
@@ -1952,4 +1971,40 @@ mod notifications_tests {
|
||||
Notifications::Custom(ref v) if v == &vec!["foo".to_string()]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_step_notifications_default_true() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
notifications = false
|
||||
"#;
|
||||
let parsed: RootTomlTest =
|
||||
toml::from_str(toml).expect("deserialize [tui] with only notifications");
|
||||
assert!(
|
||||
parsed.tui.plan_step_notifications,
|
||||
"plan_step_notifications should default to true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_plan_step_notifications_by_default() {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
[tui]
|
||||
notifications = false
|
||||
"#,
|
||||
)
|
||||
.expect("parse config toml");
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
temp.path().to_path_buf(),
|
||||
)
|
||||
.expect("load config");
|
||||
assert!(
|
||||
config.plan_step_notifications,
|
||||
"plan_step_notifications should default to true in Config"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,13 @@ pub struct Tui {
|
||||
/// Enable desktop notifications from the TUI when the terminal is unfocused.
|
||||
/// Defaults to `false`.
|
||||
pub notifications: Notifications,
|
||||
/// When true, emit notifications as plan steps are completed. Defaults to `true`.
|
||||
#[serde(default = "true_bool")]
|
||||
pub plan_step_notifications: bool,
|
||||
}
|
||||
|
||||
const fn true_bool() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
|
||||
@@ -90,6 +90,50 @@ pub(crate) async fn handle_update_plan(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CompletedStep {
|
||||
pub step: String,
|
||||
pub position: usize,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
pub fn newly_completed_steps(
|
||||
previous: &[PlanItemArg],
|
||||
current: &[PlanItemArg],
|
||||
) -> Vec<CompletedStep> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut prev_status: HashMap<&str, &StepStatus> = HashMap::new();
|
||||
for item in previous {
|
||||
let step = item.step.trim();
|
||||
if !step.is_empty() {
|
||||
prev_status.insert(step, &item.status);
|
||||
}
|
||||
}
|
||||
|
||||
let total = current.len();
|
||||
let mut completed = Vec::new();
|
||||
for (idx, item) in current.iter().enumerate() {
|
||||
let step = item.step.trim();
|
||||
if step.is_empty() || !matches!(item.status, StepStatus::Completed) {
|
||||
continue;
|
||||
}
|
||||
let was_completed = prev_status
|
||||
.get(step)
|
||||
.map(|status| matches!(status, StepStatus::Completed))
|
||||
.unwrap_or(false);
|
||||
if !was_completed {
|
||||
completed.push(CompletedStep {
|
||||
step: step.to_owned(),
|
||||
position: idx + 1,
|
||||
total,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
completed
|
||||
}
|
||||
|
||||
fn parse_update_plan_arguments(
|
||||
arguments: String,
|
||||
call_id: &str,
|
||||
@@ -108,3 +152,63 @@ fn parse_update_plan_arguments(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_step(step: &str, status: StepStatus) -> PlanItemArg {
|
||||
PlanItemArg {
|
||||
step: step.to_string(),
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_newly_completed_steps() {
|
||||
let prev = vec![
|
||||
make_step("Explore", StepStatus::Completed),
|
||||
make_step("Implement", StepStatus::InProgress),
|
||||
];
|
||||
let current = vec![
|
||||
make_step("Explore", StepStatus::Completed),
|
||||
make_step("Implement", StepStatus::Completed),
|
||||
make_step("Document", StepStatus::Pending),
|
||||
];
|
||||
|
||||
let completed = newly_completed_steps(&prev, ¤t);
|
||||
assert_eq!(
|
||||
completed,
|
||||
vec![CompletedStep {
|
||||
step: "Implement".to_string(),
|
||||
position: 2,
|
||||
total: 3,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_already_completed_steps() {
|
||||
let prev = vec![make_step("Explore", StepStatus::Completed)];
|
||||
let current = vec![make_step("Explore", StepStatus::Completed)];
|
||||
|
||||
let completed = newly_completed_steps(&prev, ¤t);
|
||||
assert!(completed.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace_in_step_names() {
|
||||
let prev = vec![make_step(" Implement ", StepStatus::InProgress)];
|
||||
let current = vec![make_step("Implement", StepStatus::Completed)];
|
||||
|
||||
let completed = newly_completed_steps(&prev, ¤t);
|
||||
assert_eq!(
|
||||
completed,
|
||||
vec![CompletedStep {
|
||||
step: "Implement".to_string(),
|
||||
position: 1,
|
||||
total: 1,
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ pub(crate) struct ChatComposer {
|
||||
attached_images: Vec<AttachedImage>,
|
||||
placeholder_text: String,
|
||||
is_task_running: bool,
|
||||
current_plan_step: Option<String>,
|
||||
// Non-bracketed paste burst tracker.
|
||||
paste_burst: PasteBurst,
|
||||
// When true, disables paste-burst logic and inserts characters immediately.
|
||||
@@ -127,6 +128,7 @@ impl ChatComposer {
|
||||
attached_images: Vec::new(),
|
||||
placeholder_text,
|
||||
is_task_running: false,
|
||||
current_plan_step: None,
|
||||
paste_burst: PasteBurst::default(),
|
||||
disable_paste_burst: false,
|
||||
custom_prompts: Vec::new(),
|
||||
@@ -177,6 +179,14 @@ impl ChatComposer {
|
||||
self.token_usage_info = token_info;
|
||||
}
|
||||
|
||||
pub(crate) fn set_current_plan_step(&mut self, step: Option<String>) -> bool {
|
||||
if self.current_plan_step == step {
|
||||
return false;
|
||||
}
|
||||
self.current_plan_step = step;
|
||||
true
|
||||
}
|
||||
|
||||
/// Record the history metadata advertised by `SessionConfiguredEvent` so
|
||||
/// that the composer can navigate cross-session history.
|
||||
pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) {
|
||||
@@ -1300,6 +1310,13 @@ impl WidgetRef for ChatComposer {
|
||||
hint.push(" edit prev".into());
|
||||
}
|
||||
|
||||
if let Some(step) = &self.current_plan_step {
|
||||
hint.push(" ".into());
|
||||
hint.push("Now:".dim());
|
||||
hint.push(" ".into());
|
||||
hint.push(step.clone().cyan().bold());
|
||||
}
|
||||
|
||||
// Append token/context usage info to the footer hints when available.
|
||||
if let Some(token_usage_info) = &self.token_usage_info {
|
||||
let token_usage = &token_usage_info.total_token_usage;
|
||||
@@ -1436,6 +1453,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footer_hint_includes_current_plan_step() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
assert!(composer.set_current_plan_step(Some("Implement feature".to_string())));
|
||||
|
||||
let area = Rect::new(0, 0, 80, 6);
|
||||
let mut buf = Buffer::empty(area);
|
||||
composer.render_ref(area, &mut buf);
|
||||
|
||||
let bottom_row: String = (0..area.width)
|
||||
.map(|x| {
|
||||
buf[(x, area.height - 1)]
|
||||
.symbol()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap_or(' ')
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
bottom_row.contains("Now:"),
|
||||
"missing status label: {bottom_row:?}"
|
||||
);
|
||||
assert!(
|
||||
bottom_row.contains("Implement feature"),
|
||||
"missing plan text: {bottom_row:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_current_at_token_basic_cases() {
|
||||
let test_cases = vec![
|
||||
|
||||
@@ -373,6 +373,12 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn set_plan_progress(&mut self, step: Option<String>) {
|
||||
if self.composer.set_current_plan_step(step) {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when the agent requests user approval.
|
||||
pub fn push_approval_request(&mut self, request: ApprovalRequest) {
|
||||
let request = if let Some(view) = self.active_view.as_mut() {
|
||||
|
||||
@@ -5,6 +5,9 @@ use std::sync::Arc;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_types::Notifications;
|
||||
use codex_core::plan_tool::PlanItemArg;
|
||||
use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::plan_tool::newly_completed_steps;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
use codex_core::protocol::AgentReasoningDeltaEvent;
|
||||
@@ -141,6 +144,7 @@ pub(crate) struct ChatWidget {
|
||||
queued_user_messages: VecDeque<UserMessage>,
|
||||
// Pending notification to show when unfocused on next Draw
|
||||
pending_notification: Option<Notification>,
|
||||
last_plan: Vec<PlanItemArg>,
|
||||
}
|
||||
|
||||
struct UserMessage {
|
||||
@@ -172,6 +176,8 @@ impl ChatWidget {
|
||||
}
|
||||
// --- Small event handlers ---
|
||||
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
||||
self.bottom_pane.set_plan_progress(None);
|
||||
self.last_plan.clear();
|
||||
self.bottom_pane
|
||||
.set_history_metadata(event.history_log_id, event.history_entry_count);
|
||||
self.conversation_id = Some(event.session_id);
|
||||
@@ -325,6 +331,19 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_plan_update(&mut self, update: codex_core::plan_tool::UpdatePlanArgs) {
|
||||
let completed_steps = newly_completed_steps(&self.last_plan, &update.plan);
|
||||
let current_step = select_current_plan_step(&update.plan);
|
||||
self.bottom_pane.set_plan_progress(current_step);
|
||||
if self.config.plan_step_notifications {
|
||||
for completed in &completed_steps {
|
||||
self.notify(Notification::PlanStepComplete {
|
||||
step: completed.step.clone(),
|
||||
position: completed.position,
|
||||
total: completed.total,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.last_plan = update.plan.clone();
|
||||
self.add_to_history(history_cell::new_plan_update(update));
|
||||
}
|
||||
|
||||
@@ -700,6 +719,7 @@ impl ChatWidget {
|
||||
show_welcome_banner: true,
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
last_plan: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,6 +776,7 @@ impl ChatWidget {
|
||||
show_welcome_banner: true,
|
||||
suppress_session_configured_redraw: true,
|
||||
pending_notification: None,
|
||||
last_plan: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,7 +1174,12 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn notify(&mut self, notification: Notification) {
|
||||
if !notification.allowed_for(&self.config.tui_notifications) {
|
||||
let allow = match &self.config.tui_notifications {
|
||||
Notifications::Enabled(enabled) => *enabled,
|
||||
custom => notification.allowed_for(custom),
|
||||
};
|
||||
|
||||
if !allow {
|
||||
return;
|
||||
}
|
||||
self.pending_notification = Some(notification);
|
||||
@@ -1478,10 +1504,21 @@ impl WidgetRef for &ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Notification {
|
||||
AgentTurnComplete,
|
||||
ExecApprovalRequested { command: String },
|
||||
EditApprovalRequested { cwd: PathBuf, changes: Vec<PathBuf> },
|
||||
ExecApprovalRequested {
|
||||
command: String,
|
||||
},
|
||||
EditApprovalRequested {
|
||||
cwd: PathBuf,
|
||||
changes: Vec<PathBuf>,
|
||||
},
|
||||
PlanStepComplete {
|
||||
step: String,
|
||||
position: usize,
|
||||
total: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
@@ -1502,6 +1539,18 @@ impl Notification {
|
||||
}
|
||||
)
|
||||
}
|
||||
Notification::PlanStepComplete {
|
||||
step,
|
||||
position,
|
||||
total,
|
||||
} => {
|
||||
format!(
|
||||
"Plan step complete ({}/{}): {}",
|
||||
position,
|
||||
total,
|
||||
truncate_text(step, 40)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1510,6 +1559,7 @@ impl Notification {
|
||||
Notification::AgentTurnComplete => "agent-turn-complete",
|
||||
Notification::ExecApprovalRequested { .. }
|
||||
| Notification::EditApprovalRequested { .. } => "approval-requested",
|
||||
Notification::PlanStepComplete { .. } => "plan-step-complete",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1560,5 +1610,37 @@ fn extract_first_bold(s: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn select_current_plan_step(plan: &[PlanItemArg]) -> Option<String> {
|
||||
let in_progress = plan.iter().find_map(|item| {
|
||||
if matches!(item.status, StepStatus::InProgress) {
|
||||
let trimmed = item.step.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if in_progress.is_some() {
|
||||
return in_progress;
|
||||
}
|
||||
|
||||
plan.iter().find_map(|item| {
|
||||
if matches!(item.status, StepStatus::Pending) {
|
||||
let trimmed = item.step.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
|
||||
@@ -252,6 +252,7 @@ fn make_chatwidget_manual() -> (
|
||||
queued_user_messages: VecDeque::new(),
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
last_plan: Vec::new(),
|
||||
};
|
||||
(widget, rx, op_rx)
|
||||
}
|
||||
@@ -1496,6 +1497,74 @@ fn plan_update_renders_history_cell() {
|
||||
assert!(blob.contains("Write tests"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_completion_notification_emitted_when_enabled() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
chat.config.plan_step_notifications = true;
|
||||
chat.config.tui_notifications = Notifications::Enabled(true);
|
||||
|
||||
let seed_plan = UpdatePlanArgs {
|
||||
explanation: None,
|
||||
plan: vec![PlanItemArg {
|
||||
step: "Implement feature".into(),
|
||||
status: StepStatus::InProgress,
|
||||
}],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-seed".into(),
|
||||
msg: EventMsg::PlanUpdate(seed_plan),
|
||||
});
|
||||
drain_insert_history(&mut rx);
|
||||
assert!(chat.pending_notification.is_none());
|
||||
|
||||
let completion = UpdatePlanArgs {
|
||||
explanation: None,
|
||||
plan: vec![PlanItemArg {
|
||||
step: "Implement feature".into(),
|
||||
status: StepStatus::Completed,
|
||||
}],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-complete".into(),
|
||||
msg: EventMsg::PlanUpdate(completion),
|
||||
});
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
match chat.pending_notification {
|
||||
Some(Notification::PlanStepComplete {
|
||||
ref step,
|
||||
position,
|
||||
total,
|
||||
}) => {
|
||||
assert_eq!(step, "Implement feature");
|
||||
assert_eq!(position, 1);
|
||||
assert_eq!(total, 1);
|
||||
}
|
||||
other => panic!("expected plan completion notification, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_completion_notification_respects_toggle() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
chat.config.plan_step_notifications = false;
|
||||
chat.config.tui_notifications = Notifications::Enabled(true);
|
||||
|
||||
let update = UpdatePlanArgs {
|
||||
explanation: None,
|
||||
plan: vec![PlanItemArg {
|
||||
step: "Implement feature".into(),
|
||||
status: StepStatus::Completed,
|
||||
}],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub".into(),
|
||||
msg: EventMsg::PlanUpdate(update),
|
||||
});
|
||||
drain_insert_history(&mut rx);
|
||||
assert!(chat.pending_notification.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_error_is_rendered_to_history() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
@@ -510,7 +510,7 @@ notify = ["python3", "/Users/mbolin/.codex/notify.py"]
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Use `notify` for automation and integrations: Codex invokes your external program with a single JSON argument for each event, independent of the TUI. If you only want lightweight desktop notifications while using the TUI, prefer `tui.notifications`, which uses terminal escape codes and requires no external program. You can enable both; `tui.notifications` covers in‑TUI alerts (e.g., approval prompts), while `notify` is best for system‑level hooks or custom notifiers. Currently, `notify` emits only `agent-turn-complete`, whereas `tui.notifications` supports `agent-turn-complete` and `approval-requested` with optional filtering.
|
||||
> Use `notify` for automation and integrations: Codex invokes your external program with a single JSON argument for each event, independent of the TUI. If you only want lightweight desktop notifications while using the TUI, prefer `tui.notifications`, which uses terminal escape codes and requires no external program. You can enable both; `tui.notifications` covers in‑TUI alerts (e.g., approval prompts), while `notify` is best for system-level hooks or custom notifiers. Currently, `notify` emits only `agent-turn-complete`, whereas `tui.notifications` supports `agent-turn-complete`, `approval-requested`, and `plan-step-complete` with optional filtering.
|
||||
|
||||
## history
|
||||
|
||||
@@ -589,8 +589,12 @@ Options that are specific to the TUI.
|
||||
notifications = true
|
||||
|
||||
# You can optionally filter to specific notification types.
|
||||
# Available types are "agent-turn-complete" and "approval-requested".
|
||||
# Available types are "agent-turn-complete", "approval-requested", and "plan-step-complete".
|
||||
notifications = [ "agent-turn-complete", "approval-requested" ]
|
||||
|
||||
# Emit notifications when a plan step transitions to completed.
|
||||
# Defaults to true; set to false to silence plan updates.
|
||||
plan_step_notifications = true
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
@@ -615,6 +619,7 @@ notifications = [ "agent-turn-complete", "approval-requested" ]
|
||||
| `sandbox_workspace_write.exclude_slash_tmp` | boolean | Exclude `/tmp` from writable roots (default: false). |
|
||||
| `disable_response_storage` | boolean | Required for ZDR orgs. |
|
||||
| `notify` | array<string> | External program for notifications. |
|
||||
| `tui.plan_step_notifications` | boolean | Notify when plan steps complete (default: true). |
|
||||
| `instructions` | string | Currently ignored; use `experimental_instructions_file` or `AGENTS.md`. |
|
||||
| `mcp_servers.<id>.command` | string | MCP server launcher command. |
|
||||
| `mcp_servers.<id>.args` | array<string> | MCP server args. |
|
||||
|
||||
Reference in New Issue
Block a user