feat: add auto lookback mode (#7)

* feat: add auto lookback mode

Automatically shows full terminal history after idle timeout,
returns to truncated view when new output arrives.

- Add auto_lookback_timeout_ms config option (default: 1000ms)
- Add --auto-lookback-timeout CLI flag
- Set to 0 to disable the feature

* intermediate

* auto lookback

---------

Co-authored-by: Ilya Sinkin <ilyasinkin2002@yandex.kz>
This commit is contained in:
David Beesley
2026-01-19 18:33:24 +13:00
committed by GitHub
parent a824eaea77
commit e0c04ae8dd
5 changed files with 60 additions and 0 deletions

View File

@@ -48,6 +48,8 @@ Press `Ctrl+6` (or your configured key) to enter lookback mode:
When you exit lookback mode, any cached output is processed and the current state is displayed.
**Auto-lookback**: After 5 seconds of idle (no new renders), the full history is automatically displayed so you can scroll back without pressing any keys.
## Configuration
Create `~/.config/claude-chill.toml`:

View File

@@ -42,10 +42,15 @@ fn main() -> ExitCode {
}
};
let auto_lookback_timeout_ms = cli
.auto_lookback_timeout
.unwrap_or(config.auto_lookback_timeout_ms);
let proxy_config = ProxyConfig {
max_history_lines: history_lines,
lookback_key,
lookback_sequence,
auto_lookback_timeout_ms,
};
let cmd_args: Vec<&str> = cli.args.iter().map(|s| s.as_str()).collect();

View File

@@ -13,4 +13,7 @@ pub struct Cli {
#[arg(short = 'k', long = "lookback-key")]
pub lookback_key: Option<String>,
#[arg(long = "auto-lookback-timeout")]
pub auto_lookback_timeout: Option<u64>,
}

View File

@@ -5,6 +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;
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
@@ -12,6 +13,7 @@ pub struct Config {
pub history_lines: usize,
pub lookback_key: String,
pub refresh_rate: u64,
pub auto_lookback_timeout_ms: u64,
}
impl Default for Config {
@@ -20,6 +22,7 @@ impl Default for Config {
history_lines: 100_000,
lookback_key: DEFAULT_LOOKBACK_KEY.to_string(),
refresh_rate: DEFAULT_REFRESH_RATE,
auto_lookback_timeout_ms: DEFAULT_AUTO_LOOKBACK_TIMEOUT_MS,
}
}
}
@@ -97,6 +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);
}
#[test]

View File

@@ -48,6 +48,7 @@ pub struct ProxyConfig {
pub max_history_lines: usize,
pub lookback_key: String,
pub lookback_sequence: Vec<u8>,
pub auto_lookback_timeout_ms: u64,
}
impl Default for ProxyConfig {
@@ -56,6 +57,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,
}
}
}
@@ -95,6 +97,8 @@ pub struct Proxy {
vt_parser: vt100::Parser,
vt_prev_screen: Option<vt100::Screen>,
last_output_time: Option<Instant>,
last_render_time: Option<Instant>,
auto_lookback_timeout: Duration,
sync_buffer: Vec<u8>,
in_sync_block: bool,
in_lookback_mode: bool,
@@ -161,6 +165,8 @@ impl Proxy {
history.push_bytes(CLEAR_SCREEN);
history.push_bytes(CURSOR_HOME);
let auto_lookback_timeout = Duration::from_millis(config.auto_lookback_timeout_ms);
debug!("Proxy::spawn: command={} args={:?}", command, args);
Ok(Self {
@@ -172,6 +178,8 @@ impl Proxy {
vt_parser,
vt_prev_screen: None,
last_output_time: None,
last_render_time: None,
auto_lookback_timeout,
sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY),
in_sync_block: false,
in_lookback_mode: false,
@@ -224,6 +232,7 @@ impl Proxy {
match poll(&mut poll_fds, PollTimeout::from(poll_timeout_ms)) {
Ok(0) => {
self.flush_pending_vt_render(&stdout_fd)?;
self.check_auto_lookback(&stdout_fd)?;
continue;
}
Ok(_) => {}
@@ -551,6 +560,43 @@ impl Proxy {
// Store current screen for next diff
self.vt_prev_screen = Some(self.vt_parser.screen().clone());
self.vt_render_pending = false;
self.last_render_time = Some(Instant::now());
Ok(())
}
fn check_auto_lookback<F: AsFd>(&mut self, stdout_fd: &F) -> Result<()> {
if self.auto_lookback_timeout.is_zero() {
return Ok(());
}
if self.in_lookback_mode || self.in_alternate_screen {
return Ok(());
}
let Some(render_time) = self.last_render_time else {
return Ok(());
};
if render_time.elapsed() < self.auto_lookback_timeout {
return Ok(());
}
self.dump_history(stdout_fd)?;
self.last_render_time = None;
Ok(())
}
fn dump_history<F: AsFd>(&mut self, stdout_fd: &F) -> Result<()> {
debug!(
"dump_history: history_bytes={} lines={}",
self.history.total_bytes(),
self.history.line_count()
);
self.output_buffer.clear();
self.history.append_all(&mut self.output_buffer);
write_all(stdout_fd, CLEAR_SCREEN)?;
write_all(stdout_fd, CURSOR_HOME)?;
write_all(stdout_fd, &self.output_buffer)?;
// Force full VT render on next output since terminal now shows history
self.vt_prev_screen = None;
Ok(())
}