diff --git a/core/Cargo.lock b/core/Cargo.lock index 4bf8542..e68e68f 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -138,6 +138,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -160,6 +169,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -282,6 +297,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -296,6 +322,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -702,6 +740,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -726,6 +776,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.4" @@ -836,7 +901,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relay" -version = "1.1.0" +version = "1.2.0" dependencies = [ "anyhow", "blake3", @@ -845,6 +910,7 @@ dependencies = [ "clap_complete", "colored", "console", + "ctrlc", "dialoguer", "indicatif", "regex", diff --git a/core/Cargo.toml b/core/Cargo.toml index 15f48a8..c6647e0 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -46,6 +46,9 @@ ureq = { version = "2", features = ["json"] } # Database rusqlite = { version = "0.31", features = ["bundled"] } +# Signal handling +ctrlc = "3" + # Terminal UI colored = "2" indicatif = "0.17" diff --git a/core/src/watch.rs b/core/src/watch.rs index fe22928..09ffbaf 100644 --- a/core/src/watch.rs +++ b/core/src/watch.rs @@ -3,6 +3,8 @@ use anyhow::Result; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant}; pub struct WatchConfig { @@ -32,10 +34,19 @@ pub fn run_watch( ); eprintln!(" Press Ctrl-C to stop.\n"); + // Graceful shutdown via signal handler + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }).ok(); + + let watch_start = Instant::now(); + let mut handoff_count: u32 = 0; let mut last_handoff: Option = None; let mut last_size: u64 = 0; - loop { + while running.load(Ordering::SeqCst) { std::thread::sleep(watch_config.poll_interval); // Find latest JSONL @@ -106,6 +117,7 @@ pub fn run_watch( let result = handoff_with_chain(config, &handoff_text, &project_dir.to_string_lossy()); if result.success { + handoff_count += 1; eprintln!(" \u{2705} Auto-handed off to {}", result.agent); if !handoff_path.as_os_str().is_empty() { eprintln!(" \u{1f4c4} Saved: {}", handoff_path.display()); @@ -128,6 +140,14 @@ pub fn run_watch( last_handoff = Some(Instant::now()); last_size = current_size; } + + // Graceful shutdown summary + let elapsed = watch_start.elapsed(); + eprintln!("\n \u{1f6d1} Watch stopped."); + eprintln!(" Uptime: {}m {}s | Handoffs: {}", + elapsed.as_secs() / 60, elapsed.as_secs() % 60, handoff_count); + + Ok(()) } /// Handoff with chain — try each agent in priority order.