diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc4cea3..2ae94a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,17 @@ Contributions welcome! Feel free to open issues or pull requests. +## Bug Reports + +When filing a bug report, please include the output of `claude-chill --version`: + +``` +$ claude-chill --version +claude-chill 0.1.0 (35ecc80) +``` + +This includes the version number and git commit hash, which helps identify exactly which code you're running. + ## Branch naming - `feature/description` diff --git a/Cargo.lock b/Cargo.lock index 7919874..491e512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,7 +133,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "claude-chill" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8373cb4..469ea13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,6 @@ members = [ ] [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2024" authors = ["David Beesley"] diff --git a/README.md b/README.md index 39b7e60..f211687 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Options: -k, --lookback-key Key to toggle lookback mode, quote to prevent glob expansion (default: "[ctrl][6]") -a, --auto-lookback-timeout - Auto-lookback timeout in ms, 0 to disable (default: 5000) + Auto-lookback timeout in ms, 0 to disable (default: 15000) -h, --help Print help -V, --version @@ -97,19 +97,21 @@ When you exit lookback mode, any cached output is processed and the current stat ## Auto-Lookback -After 5 seconds of idle (no new renders), the full history is automatically dumped to your terminal so you can scroll back without pressing any keys. This is useful for reviewing Claude's output after it finishes working. +After `auto_lookback_timeout_ms` (default 15 seconds) of idle (no user input), the full history is automatically dumped to your terminal so you can scroll back without pressing any keys. This continues to re-dump every `auto_lookback_timeout_ms` while idle. This is useful for reviewing Claude's output after it finishes working. -**Note:** The auto-lookback causes a brief screen flicker during the transition as it clears the screen and writes the history buffer. Disable with `-a 0` or adjust the timeout with `-a 10000` (10 seconds). +**Note:** The auto-lookback causes a brief screen flicker during the transition as it clears the screen and writes the history buffer. Disable with `-a 0` or adjust the timeout with `-a 30000` (30 seconds). ## Configuration -Create `~/.config/claude-chill.toml`: +Config file location: +- **Linux**: `~/.config/claude-chill.toml` +- **macOS**: `~/Library/Application Support/claude-chill.toml` ```toml -history_lines = 100000 # Max lines stored for lookback -lookback_key = "[ctrl][6]" # Key to toggle lookback mode -refresh_rate = 20 # Rendering FPS -auto_lookback_timeout_ms = 5000 # Auto-lookback after 5s idle (0 to disable) +history_lines = 100000 # Max lines stored for lookback +lookback_key = "[ctrl][6]" # Key to toggle lookback mode +refresh_rate = 20 # Rendering FPS +auto_lookback_timeout_ms = 15000 # Auto-lookback after 15s idle (0 to disable) ``` Note: History is cleared on full screen redraws, so lookback shows output since Claude's last full render. diff --git a/crates/claude-chill/build.rs b/crates/claude-chill/build.rs new file mode 100644 index 0000000..8dcf922 --- /dev/null +++ b/crates/claude-chill/build.rs @@ -0,0 +1,38 @@ +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=.git/HEAD"); + println!("cargo:rerun-if-changed=.git/index"); + + let git_hash = get_git_hash(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); +} + +fn get_git_hash() -> String { + let hash = match Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + { + Ok(output) if output.status.success() => String::from_utf8(output.stdout) + .ok() + .map(|s| s.trim().to_string()), + _ => None, + }; + + let Some(hash) = hash else { + return "git not found during build, version unavailable".to_string(); + }; + + let is_dirty = Command::new("git") + .args(["status", "--porcelain"]) + .output() + .ok() + .map(|output| !output.stdout.is_empty()) + .unwrap_or(false); + + if is_dirty { + format!("{}-dirty", hash) + } else { + hash + } +} diff --git a/crates/claude-chill/src/cli.rs b/crates/claude-chill/src/cli.rs index d9b02b8..0380fc8 100644 --- a/crates/claude-chill/src/cli.rs +++ b/crates/claude-chill/src/cli.rs @@ -1,9 +1,11 @@ use clap::Parser; +const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")"); + #[derive(Parser, Debug)] #[command( name = "claude-chill", - version, + version = VERSION, about = "A PTY proxy that tames Claude Code's massive terminal updates" )] pub struct Cli { @@ -22,7 +24,7 @@ pub struct Cli { #[arg(short = 'k', long = "lookback-key")] pub lookback_key: Option, - /// Auto-lookback timeout in ms, 0 to disable (default: 5000) + /// Auto-lookback timeout in ms, 0 to disable (default: 15000) #[arg(short = 'a', long = "auto-lookback-timeout")] pub auto_lookback_timeout: Option, } diff --git a/crates/claude-chill/src/config.rs b/crates/claude-chill/src/config.rs index ca0605b..4a4b80f 100644 --- a/crates/claude-chill/src/config.rs +++ b/crates/claude-chill/src/config.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; const DEFAULT_LOOKBACK_KEY: &str = "[ctrl][6]"; const DEFAULT_REFRESH_RATE: u64 = 20; -const DEFAULT_AUTO_LOOKBACK_TIMEOUT_MS: u64 = 5000; +const DEFAULT_AUTO_LOOKBACK_TIMEOUT_MS: u64 = 15000; #[derive(Debug, Clone, Deserialize)] #[serde(default)] @@ -100,7 +100,7 @@ mod tests { assert_eq!(config.lookback_key, "[ctrl][6]"); assert_eq!(config.refresh_rate, 20); assert_eq!(config.redraw_throttle_ms(), 50); - assert_eq!(config.auto_lookback_timeout_ms, 5000); + assert_eq!(config.auto_lookback_timeout_ms, 15000); } #[test] diff --git a/crates/claude-chill/src/proxy.rs b/crates/claude-chill/src/proxy.rs index cc7b86c..b5ec3ca 100644 --- a/crates/claude-chill/src/proxy.rs +++ b/crates/claude-chill/src/proxy.rs @@ -58,7 +58,7 @@ impl Default for ProxyConfig { max_history_lines: 100_000, lookback_key: "[ctrl][6]".to_string(), lookback_sequence: vec![0x1E], - auto_lookback_timeout_ms: 5000, + auto_lookback_timeout_ms: 15000, } } } @@ -100,6 +100,8 @@ pub struct Proxy { vt_prev_screen: Option, last_output_time: Option, last_render_time: Option, + last_stdin_time: Option, + last_auto_lookback_time: Option, auto_lookback_timeout: Duration, sync_buffer: Vec, in_sync_block: bool, @@ -182,6 +184,8 @@ impl Proxy { vt_prev_screen: None, last_output_time: None, last_render_time: None, + last_stdin_time: None, + last_auto_lookback_time: None, auto_lookback_timeout, sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY), in_sync_block: false, @@ -581,14 +585,38 @@ impl Proxy { if self.in_lookback_mode || self.in_alternate_screen { return Ok(()); } + + // Check if enough time has passed since last stdin activity + let Some(stdin_time) = self.last_stdin_time else { + return Ok(()); + }; + if stdin_time.elapsed() < self.auto_lookback_timeout { + return Ok(()); + } + + // Check if there's been new output since last auto-lookback + // AND enough time has passed since we last dumped let Some(render_time) = self.last_render_time else { return Ok(()); }; - if render_time.elapsed() < self.auto_lookback_timeout { - return Ok(()); + if let Some(last_auto) = self.last_auto_lookback_time { + let no_new_output = render_time <= last_auto; + let too_soon = last_auto.elapsed() < self.auto_lookback_timeout; + if no_new_output || too_soon { + return Ok(()); + } } + + debug!( + "auto_lookback triggered: stdin_idle={}ms render_age={}ms last_auto_age={}ms", + stdin_time.elapsed().as_millis(), + render_time.elapsed().as_millis(), + self.last_auto_lookback_time + .map(|t| t.elapsed().as_millis()) + .unwrap_or(0) + ); self.dump_history(stdout_fd)?; - self.last_render_time = None; + self.last_auto_lookback_time = Some(Instant::now()); Ok(()) } @@ -611,6 +639,8 @@ impl Proxy { } fn process_input(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> { + self.last_stdin_time = Some(Instant::now()); + if self.in_alternate_screen { return write_all(&self.pty_master, data); }