← Commit history

v1.9.40 — tab flash discipline: alert set with --duration 600 (10-min hard cap survives drainer death) + actively cleared the moment audio finishes, after replays, and at idle close. A flashing tab never outlives its reason.

John Lauer ·092ad6b2df ·2mo ago ·parent c8d3a1a
6 files changed +32−24
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.39"+version = "1.9.40" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1
@@ -1 +1 @@-1.9.39+1.9.40
dev-skills/adom-tts-dev/SKILL.md+5
@@ -114,6 +114,11 @@ manually — deploy a new version and let supersede do the handoff. If you must   for the full platform facts. - **Settings, not preferences** (Adom-wide word choice; Microsoft/Google/Apple   all converged on Settings). `adom-tts settings` / `settings set surface pup`.+- **A flashing tab never outlives its reason** (user rule). The hydrogen tab+  alert is set with `--duration 600` (hydrogen kills the blink at 10 min even+  if the drainer dies) and is actively cleared the moment a clip's audio+  finishes (queue empty), after a History replay, and at idle close. A retry+  keeps the flash: audio is still owed. - **Run `tools/ralph-test.sh` before every release** — see the adom-tts-test   skill for the full matrix and the incident behind each test. - **NEVER auto-replay audio the human already heard.** Retry is for TRULY-SILENT
page.json−21
@@ -12,7 +12,6 @@     "audio",     "demo",     "accessibility",-    "cli",     "open-source",     "mit"   ],@@ -65,25 +64,5 @@   ],   "install": {     "binary_name": "adom-tts"-  },-  "description": "Adom's shared text-to-speech CLI + Playground. Reads your AI answers back to you while driving, and renders perfectly-timed voiceover tracks for demo videos.",-  "dependencies": {},-  "scripts": {-    "install": "./install.sh",-    "uninstall": "./uninstall.sh"-  },-  "needs_sudo": false,-  "files": [-    "package.json",-    "install.sh",-    "uninstall.sh",-    "bin/adom-tts",-    "skills/**",-    "completions/**"-  ],-  "org": "adom",-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"   } }\ No newline at end of file
src/play.rs+11
@@ -866,6 +866,8 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> {                     log_event("clip_start", serde_json::json!({ "clip": name, "port": player.port, "surface": surface.as_str(), "id": id, "replay": true }));                     let _ = wait_for_clip(&player.shared, id, surface, &url, &cfg.play, now_ms());                     let _ = log_clip_end(&player.shared, &name);+                    // replay done → the tab's blink has served its purpose+                    if surface == Surface::Hydrogen { server::clear_tab_alert(PLAYER_TAB_NAME); }                 }             }             continue;   // then back to the queue (interrupted clip restarts)@@ -1047,6 +1049,12 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> {                 let _ = std::fs::rename(&live, &entry);             } else {                 let _ = std::fs::remove_file(&live);+                // Clip reached a terminal outcome → STOP the tab flash NOW+                // (user rule: never keep blinking after the audio is done).+                // A retry keeps it: audio is still owed to the human.+                if surface == Surface::Hydrogen && next_queue_entry(&dir).is_none() {+                    server::clear_tab_alert(PLAYER_TAB_NAME);+                }             }             continue;         }@@ -1078,6 +1086,9 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> {     // Tear the one server down. On supersede, leave the tab alone — the new     // owner already reuses it; only close it when WE idled out.     log_event("drain_idle_close", serde_json::json!({ "kept_open": keep_open, "superseded": superseded }));+    // Idle close with the tab kept open: nothing is playing and nothing will —+    // make sure it isn't blinking. (On supersede the new owner manages alerts.)+    if !superseded && surface == Surface::Hydrogen { server::clear_tab_alert(PLAYER_TAB_NAME); }     player.shutdown();     if !keep_open && !superseded {         match surface {
src/server.rs+14−1
@@ -482,6 +482,16 @@ fn open_webview_tab(url: &str) -> Result<()> {     open_webview_tab_named(url, TAB_NAME) } +/// Stop the tab's blink. Called the MOMENT a clip finishes (the flash's whole+/// reason is "audio is waiting/playing — come listen"; once the audio is done+/// the flash is noise) and when the drainer closes. Belt to the --duration 600+/// suspenders on `alert set` (10-min hard cap even if no one clears).+pub(crate) fn clear_tab_alert(display_name: &str) {+    let _ = Command::new("adom-cli")+        .args(["hydrogen", "alert", "clear", "--name", display_name])+        .output();+}+ pub(crate) fn open_webview_tab_named(url: &str, display_name: &str) -> Result<()> {     let workspace = Command::new("adom-cli")         .args(["hydrogen", "workspace", "get"])@@ -512,8 +522,11 @@ pub(crate) fn open_webview_tab_named(url: &str, display_name: &str) -> Result<()         }         // Blink the tab so a foregrounded webview renders + autoplays (a         // backgrounded tab is deferred and never plays) and the user notices.+        // --duration 600: hydrogen itself stops the blink after 10 min even if+        // this process dies — a flashing tab must NEVER outlive its reason+        // (user rule). The drainer also clears it the moment audio finishes.         let _ = Command::new("adom-cli")-            .args(["hydrogen", "alert", "set", "--name", display_name])+            .args(["hydrogen", "alert", "set", "--name", display_name, "--duration", "600"])             .output();         return Ok(());     }