From 91215d5f4f2e013a07de2df9e7a39ea7e3a85ea5 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Thu, 4 Jun 2026 19:56:51 -0700 Subject: [PATCH] feat(tui): harvest custom completion sound files Add completion_sound = "file" with [notifications].sound_file for Windows custom WAV completion sounds without changing the global Windows sound scheme. The Windows path uses PlaySoundW asynchronously with no default fallback. Non-Windows file mode warns and no-ops, missing paths warn once, and setting a valid path resets the missing-path warning latch so later misconfiguration is visible again. Fixes #2484 Reported by @LHqweasd Harvested from PR #2512 by @cyq1017 Co-authored-by: cyq1017 <61975706+cyq1017@users.noreply.github.com> --- CHANGELOG.md | 9 +- config.example.toml | 9 +- crates/tui/CHANGELOG.md | 9 +- crates/tui/Cargo.toml | 2 +- crates/tui/src/config.rs | 33 +++++++- crates/tui/src/tui/notifications.rs | 127 ++++++++++++++++++++++++++-- crates/tui/src/tui/ui/tests.rs | 2 + docs/CONFIGURATION.md | 19 +++-- docs/V0_9_0_EXECUTION_MAP.md | 3 +- 9 files changed, 188 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d03fc62c..f014f225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `DEEPSEEK_SEARCH_BASE_URL` as a legacy alias. Custom endpoints are gated by their configured host, do not fall back to public Bing, and report the custom host as the result source for diagnostics (#2436, #2510). +- Added `completion_sound = "file"` with `[notifications].sound_file` so + Windows users can play a custom WAV file for turn-completion sounds without + changing the global Windows sound scheme (#2484, #2512). ### Changed @@ -160,9 +163,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Thanks to **@sximelon** for reporting and fixing the saved-session resume footer hint (#2758, #2760), **@cyq1017** for the custom -DuckDuckGo-compatible search endpoint, restore-listing implementation, and -pending-input delivery-mode label work (#2510, #2513, #2532, #2054), +DuckDuckGo-compatible search endpoint, custom completion sound file support, +restore-listing implementation, and pending-input delivery-mode label work +(#2510, #2512, #2513, #2532, #2054), **@Artenx** for the private-search endpoint report (#2436), +**@LHqweasd** for the Windows custom notification sound request (#2484), **@wywsoor** for the broader macOS/iTerm rollback UX report (#2494), **@HUQIANTAO** for the `web_run` lock-splitting work (#2502), turn-metadata prefix-cache stability work (#2517), and project-context cache direction diff --git a/config.example.toml b/config.example.toml index f4b8bd79..a2129c05 100644 --- a/config.example.toml +++ b/config.example.toml @@ -620,21 +620,20 @@ default_text_model = "deepseek-ai/deepseek-v4-pro" # method = "auto" # auto | osc9 | bel | off # auto: OSC 9 for iTerm.app / Ghostty / WezTerm. # On macOS / Linux, falls back to BEL. -# On Windows, falls back to "off" — BEL maps to the -# system error chime (SystemAsterisk / MB_OK), which -# sounds like an error popup. Set method = "bel" -# explicitly to opt back in (#583). +# On Windows, BEL is routed through MessageBeep(MB_OK). # osc9: \x1b]9;\x07 (iTerm2-style; shows macOS notification) # bel: plain \x07 beep # off: disable entirely # threshold_secs = 30 # only notify when the turn took >= this many seconds # include_summary = false # include elapsed time + cost in the notification body -# completion_sound = "beep" # off | beep | bell — sound on turn completion (✅ marker) +# completion_sound = "beep" # off | beep | bell | file — sound on turn completion (✅ marker) +# sound_file = "E:\\google\\downloads\\notify.wav" # WAV used when completion_sound = "file" (Windows) [notifications] # method = "auto" # threshold_secs = 30 # include_summary = false # completion_sound = "beep" +# sound_file = "E:\\google\\downloads\\notify.wav" # ───────────────────────────────────────────────────────────────────────────────── # Workspace Snapshots (#137) diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index d03fc62c..f014f225 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -46,6 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `DEEPSEEK_SEARCH_BASE_URL` as a legacy alias. Custom endpoints are gated by their configured host, do not fall back to public Bing, and report the custom host as the result source for diagnostics (#2436, #2510). +- Added `completion_sound = "file"` with `[notifications].sound_file` so + Windows users can play a custom WAV file for turn-completion sounds without + changing the global Windows sound scheme (#2484, #2512). ### Changed @@ -160,9 +163,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Thanks to **@sximelon** for reporting and fixing the saved-session resume footer hint (#2758, #2760), **@cyq1017** for the custom -DuckDuckGo-compatible search endpoint, restore-listing implementation, and -pending-input delivery-mode label work (#2510, #2513, #2532, #2054), +DuckDuckGo-compatible search endpoint, custom completion sound file support, +restore-listing implementation, and pending-input delivery-mode label work +(#2510, #2512, #2513, #2532, #2054), **@Artenx** for the private-search endpoint report (#2436), +**@LHqweasd** for the Windows custom notification sound request (#2484), **@wywsoor** for the broader macOS/iTerm rollback UX report (#2494), **@HUQIANTAO** for the `web_run` lock-splitting work (#2502), turn-metadata prefix-cache stability work (#2517), and project-context cache direction diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 60ba2411..52a05004 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -100,4 +100,4 @@ objc2 = "0.6.3" objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSArray", "NSDictionary", "NSError", "NSObject", "NSString", "NSURL"] } [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.60", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging"] } +windows = { version = "0.60", features = ["Win32_Foundation", "Win32_Media_Audio", "Win32_Security", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging"] } diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 184b3da9..2f65bc51 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -915,6 +915,8 @@ pub enum CompletionSound { Beep, /// Terminal BEL character (`\x07`). Bell, + /// Play a configured WAV sound file. + File, } /// Desktop-notification configuration (OSC 9 / BEL on turn completion). @@ -922,9 +924,9 @@ pub enum CompletionSound { pub struct NotificationsConfig { /// Delivery method: `auto` | `osc9` | `bel` | `off`. Default: `auto`. /// `auto` resolves to OSC 9 for iTerm.app / Ghostty / WezTerm / Cmux - /// (detected via `$TERM_PROGRAM` then `$LC_TERMINAL`); on macOS / Linux - /// it falls back to BEL, and on Windows it falls back to `Off` so the - /// post-turn notification doesn't ring the system error chime (#583). + /// (detected via `$TERM_PROGRAM` then `$LC_TERMINAL`); otherwise it + /// falls back to BEL. On Windows the BEL path is routed through + /// `MessageBeep(MB_OK)`. /// Use `method = "osc9"` explicitly when your terminal is OSC-9 capable /// but sets neither env var (e.g. Cmux without `LC_TERMINAL`). #[serde(default)] @@ -937,10 +939,14 @@ pub struct NotificationsConfig { #[serde(default)] pub include_summary: bool, - /// Completion sound: `"off"` | `"beep"` | `"bell"`. Default: `"beep"`. + /// Completion sound: `"off"` | `"beep"` | `"bell"` | `"file"`. Default: `"beep"`. /// Plays a sound when every turn finishes (alongside the ✅ marker). #[serde(default)] pub completion_sound: CompletionSound, + + /// Path to the WAV sound file used when `completion_sound = "file"`. + #[serde(default)] + pub sound_file: Option, } fn default_snapshots_enabled() -> bool { @@ -10220,4 +10226,23 @@ model = "deepseek-ai/deepseek-v4-pro" assert_eq!(config.default_model(), "meta-llama/Llama-3-70B"); Ok(()) } + + #[test] + fn notifications_parse_custom_completion_sound_file() { + let config: Config = toml::from_str( + r#" + [notifications] + completion_sound = "file" + sound_file = "E:\\google\\downloads\\xm4114.wav" + "#, + ) + .expect("custom completion sound config should parse"); + + let notifications = config.notifications_config(); + assert_eq!(notifications.completion_sound, CompletionSound::File); + assert_eq!( + notifications.sound_file.as_deref(), + Some(std::path::Path::new("E:\\google\\downloads\\xm4114.wav")) + ); + } } diff --git a/crates/tui/src/tui/notifications.rs b/crates/tui/src/tui/notifications.rs index 84f43191..cfa0f8f6 100644 --- a/crates/tui/src/tui/notifications.rs +++ b/crates/tui/src/tui/notifications.rs @@ -17,10 +17,19 @@ use windows::Win32::System::Diagnostics::Debug::MessageBeep; use windows::Win32::UI::WindowsAndMessaging::MESSAGEBOX_STYLE; use std::io::{self, Write}; +use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicU8; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, OnceLock}; use std::time::Duration; +#[cfg(target_os = "windows")] +use std::os::windows::ffi::OsStrExt; +#[cfg(target_os = "windows")] +use windows::Win32::Media::Audio::{PlaySoundW, SND_ASYNC, SND_FILENAME, SND_NODEFAULT}; +#[cfg(target_os = "windows")] +use windows::core::PCWSTR; + /// Notification delivery method. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Method { @@ -354,18 +363,31 @@ pub fn reset_title_on_interaction() { } } -/// Completion sound mode (0 = off, 1 = beep, 2 = bell). +/// Completion sound mode (0 = off, 1 = beep, 2 = bell, 3 = file). static COMPLETION_SOUND_MODE: AtomicU8 = AtomicU8::new(1); +static COMPLETION_SOUND_FILE: OnceLock>> = OnceLock::new(); +#[cfg(not(target_os = "windows"))] +static COMPLETION_SOUND_FILE_UNSUPPORTED_WARNED: AtomicBool = AtomicBool::new(false); +static COMPLETION_SOUND_FILE_MISSING_WARNED: AtomicBool = AtomicBool::new(false); -/// Set the completion sound mode from config. -/// Call once at startup or on `/settings` change. -pub fn set_completion_sound_mode(mode: crate::config::CompletionSound) { +fn completion_sound_file_slot() -> &'static Mutex> { + COMPLETION_SOUND_FILE.get_or_init(|| Mutex::new(None)) +} + +fn set_completion_sound(mode: crate::config::CompletionSound, sound_file: Option) { let val = match mode { crate::config::CompletionSound::Off => 0u8, crate::config::CompletionSound::Beep => 1u8, crate::config::CompletionSound::Bell => 2u8, + crate::config::CompletionSound::File => 3u8, }; COMPLETION_SOUND_MODE.store(val, Ordering::SeqCst); + if let Ok(mut slot) = completion_sound_file_slot().lock() { + if sound_file.is_some() { + COMPLETION_SOUND_FILE_MISSING_WARNED.store(false, Ordering::SeqCst); + } + *slot = sound_file; + } } /// Play the configured completion sound (if not `Off`). @@ -378,6 +400,9 @@ pub fn play_completion_sound() { 2 => { bell_sound(); } + 3 => { + file_sound(); + } _ => {} } } @@ -402,6 +427,54 @@ fn bell_sound() { let _ = io::stdout().write_all(b"\x07"); } +fn configured_sound_file() -> Option { + completion_sound_file_slot() + .lock() + .ok() + .and_then(|slot| slot.clone()) +} + +#[cfg(target_os = "windows")] +fn play_sound_file(path: &Path) { + let wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + // Best-effort and async: notification sound failure should not block or + // fail a completed agent turn. + unsafe { + let _ = PlaySoundW( + PCWSTR(wide.as_ptr()), + None, + SND_FILENAME | SND_ASYNC | SND_NODEFAULT, + ); + } +} + +#[cfg(not(target_os = "windows"))] +fn play_sound_file(_path: &Path) { + if !COMPLETION_SOUND_FILE_UNSUPPORTED_WARNED.swap(true, Ordering::SeqCst) { + tracing::warn!("completion_sound = \"file\" is currently supported on Windows only"); + } +} + +fn file_sound() { + if let Some(path) = configured_sound_file() { + play_sound_file(&path); + } else if !COMPLETION_SOUND_FILE_MISSING_WARNED.swap(true, Ordering::SeqCst) { + tracing::warn!("completion_sound = \"file\" requires [notifications].sound_file"); + } +} + +#[cfg(test)] +fn completion_sound_state_for_tests() -> (crate::config::CompletionSound, Option) { + let mode = match COMPLETION_SOUND_MODE.load(Ordering::SeqCst) { + 0 => crate::config::CompletionSound::Off, + 1 => crate::config::CompletionSound::Beep, + 2 => crate::config::CompletionSound::Bell, + 3 => crate::config::CompletionSound::File, + _ => crate::config::CompletionSound::Off, + }; + (mode, configured_sound_file()) +} + /// Show a macOS Notification Center alert via `osascript`. /// /// Runs on a dedicated background thread so the caller is not blocked. @@ -585,7 +658,7 @@ use crate::tui::app::App; pub fn settings(config: &crate::config::Config) -> Option<(Method, Duration, bool)> { let notif = config.notifications_config(); // Initialize completion sound mode from config. - set_completion_sound_mode(notif.completion_sound); + set_completion_sound(notif.completion_sound, notif.sound_file); let method = match notif.method { crate::config::NotificationMethod::Auto => Method::Auto, crate::config::NotificationMethod::Osc9 => Method::Osc9, @@ -1187,4 +1260,48 @@ mod tests { "3w 2d" ); } + + #[test] + fn settings_installs_custom_completion_sound_file() { + let config: crate::config::Config = toml::from_str( + r#" + [notifications] + completion_sound = "file" + sound_file = "E:\\google\\downloads\\xm4114.wav" + "#, + ) + .expect("custom completion sound config should parse"); + + let _ = settings(&config); + + let (mode, file) = completion_sound_state_for_tests(); + assert_eq!(mode, crate::config::CompletionSound::File); + assert_eq!( + file.as_deref(), + Some(std::path::Path::new("E:\\google\\downloads\\xm4114.wav")) + ); + } + + #[test] + fn setting_valid_sound_file_resets_missing_file_warning_latch() { + let _lock = env_lock(); + COMPLETION_SOUND_FILE_MISSING_WARNED.store(true, Ordering::SeqCst); + + set_completion_sound( + crate::config::CompletionSound::File, + Some(std::path::PathBuf::from( + "E:\\google\\downloads\\xm4114.wav", + )), + ); + + assert!(!COMPLETION_SOUND_FILE_MISSING_WARNED.load(Ordering::SeqCst)); + + set_completion_sound(crate::config::CompletionSound::File, None); + file_sound(); + + assert!(COMPLETION_SOUND_FILE_MISSING_WARNED.load(Ordering::SeqCst)); + + set_completion_sound(crate::config::CompletionSound::Beep, None); + COMPLETION_SOUND_FILE_MISSING_WARNED.store(false, Ordering::SeqCst); + } } diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 03f26e5e..5106859d 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -8369,6 +8369,7 @@ fn notification_settings_tui_always_keeps_configured_method_no_threshold() { method: crate::config::NotificationMethod::Bel, threshold_secs: 120, completion_sound: crate::config::CompletionSound::Beep, + sound_file: None, include_summary: true, }), ..Config::default() @@ -8401,6 +8402,7 @@ fn notification_settings_no_tui_override_uses_notifications_block() { method: crate::config::NotificationMethod::Osc9, threshold_secs: 45, completion_sound: crate::config::CompletionSound::Beep, + sound_file: None, include_summary: false, }), ..Config::default() diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 4e75b264..42cd3bd4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -951,15 +951,18 @@ If you are upgrading from older releases: 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). + `bel`; on Windows the BEL path is routed through `MessageBeep(MB_OK)`. - `[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 cost in the configured display currency. +- `[notifications].completion_sound` (string, optional): `off`, `beep`, + `bell`, or `file`. Defaults to `beep`. `file` plays the WAV path from + `[notifications].sound_file` on Windows. +- `[notifications].sound_file` (path, optional): path to a custom WAV file + used when `completion_sound = "file"`. - `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. This is retained for config compatibility, but interactive sessions now always use the TUI-owned alternate screen so host terminal scrollback cannot hijack the viewport. - `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies only transcript text, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right sidebar and include visual wraps because the terminal, not the TUI, owns the 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. @@ -1022,16 +1025,22 @@ The TUI can emit a desktop notification (OSC 9 escape or plain BEL) when a turn method = "auto" # auto | osc9 | bel | off threshold_secs = 30 # only notify when the turn took >= this many seconds include_summary = false # include elapsed time + cost in the notification body +completion_sound = "beep" # off | beep | bell | file +sound_file = "E:\\google\\downloads\\notify.wav" # for completion_sound = "file" ``` Method semantics: -- `auto` (default) — picks `osc9` for `iTerm.app`, `Ghostty`, and `WezTerm` (detected via `$TERM_PROGRAM`). On macOS and Linux it falls back to `bel`. **On Windows the fallback is `off`** instead of `bel`, because the Windows audio stack maps `\x07` to the `SystemAsterisk` / `MB_OK` chime — the same sound application error popups use, so a successful-turn notification ends up sounding like an error (#583). +- `auto` (default) — picks `osc9` for `iTerm.app`, `Ghostty`, and `WezTerm` (detected via `$TERM_PROGRAM`). Otherwise it falls back to `bel`; on Windows that BEL path is routed through `MessageBeep(MB_OK)`. - `osc9` — emit `\x1b]9;\x07`. Inside tmux the sequence is wrapped in DCS passthrough so it reaches the outer terminal. - `bel` — emit a single `\x07` byte. Use this on Windows only if you actively want the chime back. - `off` — disable post-turn notifications entirely. -Windows users who run inside a known OSC-9 terminal (e.g. WezTerm on Windows) keep getting OSC-9 notifications; the `off` fallback only applies when no recognised `TERM_PROGRAM` is detected. +Windows users who run inside a known OSC-9 terminal (e.g. WezTerm on Windows) keep getting OSC-9 notifications. Set `method = "off"` to disable threshold-based desktop notifications entirely. + +`completion_sound = "file"` is for Windows users who want a per-application +completion sound without changing the global Windows sound scheme. It plays the +configured WAV `sound_file` asynchronously via the native Windows audio API. ### Parsed but currently unused (reserved for future versions) diff --git a/docs/V0_9_0_EXECUTION_MAP.md b/docs/V0_9_0_EXECUTION_MAP.md index a6462e83..672422ae 100644 --- a/docs/V0_9_0_EXECUTION_MAP.md +++ b/docs/V0_9_0_EXECUTION_MAP.md @@ -65,6 +65,7 @@ harvest/stewardship commits: | #2530 mention depth-cap hint | Already present; original closed on 2026-06-05 after public integration branch. | Present in the current v0.9 stack as `a97675824` and `29f57665e`. `cargo test -p codewhale-tui --locked try_autocomplete_file_mention_no_match` passed. | | #2513 restore snapshot listing | Harvested; original closed on 2026-06-05 after public integration branch. | Manually harvested as `311eb4002` with explicit `/restore list 101` cap rejection. `cargo test -p codewhale-tui --locked restore_`; `cargo fmt --all -- --check`; `cargo clippy -p codewhale-tui --locked -- -D warnings` passed. Keep #2494 open because this is only the restore-listing slice. | | #2510 custom DuckDuckGo-compatible endpoint | Harvested into a focused review branch; close original after review PR lands. | Adds `[search].base_url`, preferred `CODEWHALE_SEARCH_BASE_URL`, and legacy `DEEPSEEK_SEARCH_BASE_URL` for private DDG-compatible HTML endpoints. Network policy gates the configured host, custom endpoints do not fall back to public Bing, non-DDG provider/base_url combinations and challenge pages return explicit errors, and custom results report the configured host as `source`. Credit @cyq1017 for #2510 and @Artenx for the DDG-style endpoint clarification in #2436. | +| #2512 custom completion sound files | Harvested into a focused review branch; close original after review PR lands. | Adds `completion_sound = "file"` plus `[notifications].sound_file` so Windows users can play a per-app custom WAV through `PlaySoundW(SND_FILENAME | SND_ASYNC | SND_NODEFAULT)`. Non-Windows file mode warns and no-ops, missing paths warn once, and setting a valid path resets the missing-path warning latch so later misconfiguration is visible again. Credit @cyq1017 for #2512 and @LHqweasd for the Windows custom notification request in #2484. | | #2576 PrefixCacheChange first-freeze event | Already present; original closed on 2026-06-05 after public integration branch. | Present in the current v0.9 stack through `29acb87a9d`. `cargo test -p codewhale-tui --locked prefix_cache` passed. | | #2502 web_run RwLock split | Harvested; original closed on 2026-06-05 after public integration branch. | Manually harvested as `60f8e7d62` with panic-safe state write-back, `Arc` cache reads, and serialized cache tests. `cargo test -p codewhale-tui --locked web_run`; `cargo clippy -p codewhale-tui --locked -- -D warnings`; `cargo fmt --all -- --check` passed. | | #2517 turn_meta tail relocation | Manually harvested with the user-text content block first and volatile turn metadata last. | `cargo test -p codewhale-tui --locked turn_metadata`; `cargo test -p codewhale-tui --locked user_message_turn_meta_is_appended_not_prepended`; `cargo test -p codewhale-tui --locked post_edit_hook_injects_diagnostics_message_before_next_request`; `cargo test -p codewhale-tui --locked request_builder_keeps_tail_turn_meta_after_user_text_for_wire`; `cargo clippy -p codewhale-tui --locked -- -D warnings` passed. | @@ -117,7 +118,7 @@ v0.9 branch so the remaining Windows/manual checks are explicit. | #2509 parallel read-only web search | Closed / already merged via #2504 | Already present in `origin/main` as `a09af2024`; closed as harvested/superseded on 2026-06-04. | | #2510 custom DuckDuckGo endpoint | Draft/mergeable / harvested in focused branch | Close/comment after the focused review PR lands. Keep credit for @cyq1017 and issue reporter @Artenx. | | #2511 ToolCallBefore hooks | Conflicting | Defer to hook lifecycle lane. | -| #2512 custom completion sounds | Draft/conflicting | Defer. | +| #2512 custom completion sounds | Draft/conflicting / harvested in focused branch | Close/comment after the focused review PR lands. Keep credit for @cyq1017 and issue reporter @LHqweasd. | | #2513 restore snapshot listing | Closed / harvested | Manually harvested as `311eb4002` with cap-rejection polish; original closed on 2026-06-05, leave #2494 open. | | #2517 turn_meta tail relocation | Mergeable | Manually harvested on the v0.9 branch; close/comment after branch is public. | | #2520 prompt base disk cache | Mergeable | Defer. Review found unused prompt-cache infrastructure with no runtime wiring, cache keys that still require building the prompt first, real-home cache writes in tests, and a contract that depends on the deferred #2687 prompt split. |