docs(notifications): only completed turns notify; add Key Reference + WezTerm-on-Windows test

Post-merge review feedback on #583 surfaced four small accuracy gaps:

1. The narrative docs in `docs/CONFIGURATION.md` and the inline comment
   in `config.example.toml` said the notification fires "when a turn
   takes longer than a threshold" — but the call site in
   `tui/ui.rs:928` is gated on `TurnOutcomeStatus::Completed`. Failed
   and cancelled turns are silent on purpose. Spell that out so users
   don't expect alerts on long failures.

2. The `notify_done` rustdoc still summarised `Auto` as "Osc9 for known
   terminals, Bel otherwise" — internally inconsistent with the new
   Windows-aware fallback documented one screen earlier on the
   `Method::Auto` enum and on `resolve_method`. Update the public
   rustdoc to point at the canonical resolution table on
   `resolve_method` and call out the `Off`-on-Windows branch.

3. The `## Key Reference` list in `docs/CONFIGURATION.md` had no entries
   for `[notifications].method`, `[notifications].threshold_secs`, or
   `[notifications].include_summary`. Other features with a dedicated
   subsection (e.g. `[memory].enabled`) are listed there too, so readers
   scanning the canonical key list could not discover the notification
   knobs. Added the three keys with cross-references to the
   Notifications subsection.

4. The Windows-only test only covered the unknown-`TERM_PROGRAM` →
   `Off` fallback. The positive path (known OSC-9 terminal still
   resolves to `Osc9`) was only tested via `iTerm.app`, which is a
   macOS-only program — Windows CI would still pass if the `WezTerm`
   arm of the match disappeared. Added
   `auto_detect_picks_osc9_for_wezterm_on_windows` so the
   WezTerm-on-Windows compatibility guarantee is exercised on the
   Windows runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-04 13:38:21 -05:00
parent 3636908bb9
commit a68c8dc974
3 changed files with 52 additions and 9 deletions
+4 -2
View File
@@ -281,8 +281,10 @@ default_text_model = "deepseek-ai/deepseek-v4-pro"
# ─────────────────────────────────────────────────────────────────────────────────
# Desktop Notifications (OSC 9 / BEL on long agent-turn completion)
# ─────────────────────────────────────────────────────────────────────────────────
# Emits an escape sequence to the terminal when a turn finishes and took longer
# than `threshold_secs`. Useful when you tab away from the TUI and want an alert.
# Emits an escape sequence to the terminal when a turn **completes successfully**
# and took longer than `threshold_secs`. Failed or cancelled turns are
# intentionally silent. Useful when you tab away from the TUI and want an alert
# for "your task is ready".
#
# method = "auto" # auto | osc9 | bel | off
# auto: OSC 9 for iTerm.app / Ghostty / WezTerm.
+33 -6
View File
@@ -122,9 +122,13 @@ pub fn notify_done_to<W: Write>(
/// Emit a turn-complete notification to **stdout** if `elapsed >= threshold`.
///
/// With `method = Auto`, selects `Osc9` for known capable terminals and `Bel`
/// otherwise. Pass `in_tmux = true` (i.e. `$TMUX` is non-empty at runtime)
/// to wrap OSC 9 in a DCS passthrough.
/// With `method = Auto`, selects `Osc9` for known capable terminals
/// (`iTerm.app`, `Ghostty`, `WezTerm`); the unknown-terminal fallback is
/// platform-aware — `Bel` on macOS / Linux, `Off` on Windows (where BEL
/// maps to the `SystemAsterisk` / `MB_OK` error chime, #583). See
/// [`resolve_method`] for the canonical resolution table. Pass
/// `in_tmux = true` (i.e. `$TMUX` is non-empty at runtime) to wrap OSC 9
/// in a DCS passthrough.
pub fn notify_done(
method: Method,
in_tmux: bool,
@@ -310,9 +314,7 @@ mod tests {
/// #583: on Windows, an unknown TERM_PROGRAM resolves to `Off`
/// (not `Bel`) so the post-turn notification doesn't ring the
/// `SystemAsterisk` / `MB_OK` chime. Known OSC-9 terminals like
/// WezTerm still resolve to `Osc9` — see the iTerm test, which
/// also exercises the OSC-9 branch on Windows.
/// `SystemAsterisk` / `MB_OK` chime.
#[test]
#[cfg(target_os = "windows")]
fn auto_detect_picks_off_for_unknown_on_windows() {
@@ -331,6 +333,31 @@ mod tests {
assert_eq!(resolved, Method::Off);
}
/// #583: known OSC-9 terminals must still resolve to `Osc9` on
/// Windows — the off-fallback only applies to unrecognised
/// `TERM_PROGRAM`. The cross-platform iTerm test above is a thin
/// proxy because iTerm itself only runs on macOS; if the WezTerm
/// arm of the match silently disappeared, that test would still
/// pass on the Windows runner and we'd lose the WezTerm-on-Windows
/// compatibility guarantee. Pin it directly.
#[test]
#[cfg(target_os = "windows")]
fn auto_detect_picks_osc9_for_wezterm_on_windows() {
let _lock = env_lock();
let prev = std::env::var_os("TERM_PROGRAM");
// SAFETY: test-only; serialised by env_lock().
unsafe { std::env::set_var("TERM_PROGRAM", "WezTerm") };
let resolved = resolve_method();
// SAFETY: test-only; serialised by env_lock().
unsafe {
match prev {
Some(v) => std::env::set_var("TERM_PROGRAM", v),
None => std::env::remove_var("TERM_PROGRAM"),
}
}
assert_eq!(resolved, Method::Osc9);
}
#[test]
fn humanize_duration_seconds_and_minutes() {
assert_eq!(humanize_duration(Duration::from_secs(0)), "0s");
+15 -1
View File
@@ -368,6 +368,20 @@ If you are upgrading from older releases:
- `[capacity].deepseek_v4_pro_prior` (float, default `3.5`)
- `[capacity].deepseek_v4_flash_prior` (float, default `4.2`)
- `[capacity].fallback_default_prior` (float, default `3.8`)
- `[notifications].method` (string, optional): `auto`, `osc9`, `bel`, or
`off`. Defaults to `auto`. The TUI fires this on completed (successful)
turns whose elapsed time meets `threshold_secs`; failed and cancelled
turns are silent. `auto` resolves to `osc9` for `iTerm.app`, `Ghostty`,
and `WezTerm` (detected via `$TERM_PROGRAM`). Otherwise the fallback is
`bel` on macOS / Linux and `off` on Windows (where BEL maps to the
system error chime — see the [Notifications](#notifications) section
for the full rationale, #583).
- `[notifications].threshold_secs` (int, optional): defaults to `30`.
Only completed turns whose elapsed time meets or exceeds this fire a
notification.
- `[notifications].include_summary` (bool, optional): defaults to
`false`. When `true`, the notification body includes the elapsed
duration and the turn's USD cost.
- `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. `auto` disables the alternate screen in Zellij; `--no-alt-screen` forces inline mode. Set `never` or run with `--no-alt-screen` when you want real terminal scrollback.
- `tui.mouse_capture` (bool, optional, default `true` when the alternate screen is active): enable internal mouse scrolling, transcript selection, and right-click context actions. TUI-owned drag selection copies only user/assistant transcript text. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection.
- `tui.terminal_probe_timeout_ms` (int, optional, default `500`): startup terminal-mode probe timeout in milliseconds. Values are clamped to `100..=5000`; timeout emits a warning and aborts startup instead of hanging indefinitely.
@@ -402,7 +416,7 @@ Notes:
### Notifications
The TUI can emit a desktop notification (OSC 9 escape or plain BEL) when a turn takes longer than a threshold, so you can tab away while a long task runs. Configuration lives under `[notifications]`:
The TUI can emit a desktop notification (OSC 9 escape or plain BEL) when a turn **completes successfully** and took longer than a threshold, so you can tab away while a long task runs. Failed or cancelled turns are intentionally silent — the notification is a "your task is ready" cue, not a generic ping. Configuration lives under `[notifications]`:
```toml
[notifications]