Fix auto-lookback trigger and add version tracking (#20)

* Fix auto-lookback trigger and add version tracking

- Trigger on stdin inactivity instead of render time
- Only dump when new output exists since last dump
- Default timeout 5s -> 15s
- Embed git hash in version output
- Document macOS config path
This commit is contained in:
David Beesley
2026-01-22 15:58:47 -07:00
committed by GitHub
parent 35ecc80e2f
commit 0903d8c205
8 changed files with 101 additions and 18 deletions

View File

@@ -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`

2
Cargo.lock generated
View File

@@ -133,7 +133,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "claude-chill"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"anyhow",
"clap",

View File

@@ -5,6 +5,6 @@ members = [
]
[workspace.package]
version = "0.1.0"
version = "0.1.1"
edition = "2024"
authors = ["David Beesley"]

View File

@@ -55,7 +55,7 @@ Options:
-k, --lookback-key <LOOKBACK_KEY>
Key to toggle lookback mode, quote to prevent glob expansion (default: "[ctrl][6]")
-a, --auto-lookback-timeout <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.

View File

@@ -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
}
}

View File

@@ -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<String>,
/// 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<u64>,
}

View File

@@ -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]

View File

@@ -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<vt100::Screen>,
last_output_time: Option<Instant>,
last_render_time: Option<Instant>,
last_stdin_time: Option<Instant>,
last_auto_lookback_time: Option<Instant>,
auto_lookback_timeout: Duration,
sync_buffer: Vec<u8>,
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<F: AsFd>(&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);
}