Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions src/commands/config/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1495,6 +1495,10 @@ fn render_version_check(out: &mut String) -> anyhow::Result<()> {

/// Fetch the latest release version from GitHub
fn fetch_latest_version() -> anyhow::Result<String> {
// Held for the whole lookup; the sub-startup-delay paths (test injection,
// fast failures, response parsing) return before it ever renders.
let _watchdog = worktrunk::progress::Watchdog::start("the version check", None);

// Allow tests to inject a version without network access.
// Set to "error" to simulate a fetch failure.
if let Ok(version) = std::env::var("WORKTRUNK_TEST_LATEST_VERSION") {
Expand All @@ -1508,28 +1512,25 @@ fn fetch_latest_version() -> anyhow::Result<String> {
"worktrunk/{} (https://worktrunk.dev)",
env!("CARGO_PKG_VERSION")
);
// The watchdog (below) supplies "still waiting" feedback, so the fetch needn't
// The watchdog (above) supplies "still waiting" feedback, so the fetch needn't
// be cut off at an aggressive 5s. But the watchdog is TTY-gated — a
// non-interactive run (CI, scripts, redirected stderr) gets no feedback — so a
// hard ceiling still has to exist or such a run could hang silently:
// --connect-timeout fails fast when offline, and a generous --max-time bounds a
// connected-but-stalled host without cutting off a slow-but-progressing fetch.
let output = {
let _watchdog = worktrunk::progress::Watchdog::start("the latest version", None);
Cmd::new("curl")
.args([
"--silent",
"--fail",
"--connect-timeout",
"10",
"--max-time",
"60",
"--header",
&format!("User-Agent: {user_agent}"),
"https://api.github.com/repos/max-sixty/worktrunk/releases/latest",
])
.run()?
};
let output = Cmd::new("curl")
.args([
"--silent",
"--fail",
"--connect-timeout",
"10",
"--max-time",
"60",
"--header",
&format!("User-Agent: {user_agent}"),
"https://api.github.com/repos/max-sixty/worktrunk/releases/latest",
])
.run()?;

if !output.status.success() {
anyhow::bail!("GitHub API request failed");
Expand Down
2 changes: 1 addition & 1 deletion src/commands/worktree/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ fn fetch_ref_info(
repo: &Repository,
) -> anyhow::Result<RemoteRefInfo> {
let _watchdog = worktrunk::progress::Watchdog::start(
&format!("the {} info", provider.ref_type().name()),
&format!("the {} lookup", provider.ref_type().name()),
None,
);
provider.fetch_info(number, repo)
Expand Down
10 changes: 5 additions & 5 deletions src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ pub(crate) fn render_llm_invocation(command: &str) -> anyhow::Result<String> {
/// ([`generate_summary_core`](crate::summary::generate_summary_core), up to 8
/// under a semaphore), where
/// per-call spinners would interleave.
fn watch_llm_command(command: &str, waiting_for: &str) -> worktrunk::progress::Watchdog {
fn watch_llm_command(command: &str) -> worktrunk::progress::Watchdog {
let invocation = render_llm_invocation(command).ok();
worktrunk::progress::Watchdog::start(waiting_for, invocation.as_deref())
worktrunk::progress::Watchdog::start("the commit generation command", invocation.as_deref())
}

/// Format a reproduction command, only wrapping with `sh -c` if needed.
Expand Down Expand Up @@ -667,7 +667,7 @@ pub(crate) fn generate_commit_message(
// A slow or hung command is otherwise silent (stdout is captured); the
// watchdog surfaces a "still waiting" status. Held until this function
// returns, clearing the block before the caller prints the message.
let _watchdog = watch_llm_command(command, "the commit message");
let _watchdog = watch_llm_command(command);
// Commit generation is explicitly configured - fail if it doesn't work
return try_generate_commit_message(
command,
Expand Down Expand Up @@ -841,7 +841,7 @@ pub(crate) fn generate_squash_message(

// See `generate_commit_message` — keep a slow squash-message generation
// from being silent.
let _watchdog = watch_llm_command(command, "the squash commit message");
let _watchdog = watch_llm_command(command);
return execute_llm_command(command, &prompt).map_err(|e| {
worktrunk::git::GitError::LlmCommandFailed {
command: command.clone(),
Expand Down Expand Up @@ -967,7 +967,7 @@ pub(crate) fn test_commit_generation(

// The connectivity test shells out the same way real generation does, so a
// slow command would be just as silent — surface the same waiting status.
let _watchdog = watch_llm_command(command, "the test commit message");
let _watchdog = watch_llm_command(command);
execute_llm_command(command, &prompt).map_err(|e| {
worktrunk::git::GitError::LlmCommandFailed {
command: command.clone(),
Expand Down
40 changes: 21 additions & 19 deletions src/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,13 @@ mod imp {
///
/// Unlike [`Progress`] (which counts work units), `Watchdog` just tracks
/// elapsed time. After a startup delay it renders a dim one-line status —
/// `↳ Waiting for the commit message (4s)` — redrawn in place each second;
/// the ticking counter is the "still alive" signal. After a longer delay it
/// escalates, revealing the exact command in a gutter beneath the status:
/// `↳ Waiting for the commit generation command (4s)` — redrawn in place
/// each second; the ticking counter is the "still alive" signal. After a
/// longer delay it escalates, revealing the exact command in a gutter
/// beneath the status:
///
/// ```text
/// ↳ Waiting for the commit message (12s)
/// ↳ Waiting for the commit generation command (12s)
/// sh -c 'claude -p --model=haiku'
/// ```
///
Expand All @@ -319,10 +320,11 @@ mod imp {
impl Watchdog {
/// Start a watchdog for a slow subprocess.
///
/// `waiting_for` names what is being awaited (e.g. `"the commit
/// message"`). `command`, if given, is the exact invocation — revealed
/// in a gutter beneath the status line once the command runs past the
/// escalation delay, so a slow or stuck command is debuggable.
/// `waiting_for` names the running operation (e.g. `"the commit
/// generation command"`). `command`, if given, is the exact
/// invocation — revealed in a gutter beneath the status line once the
/// command runs past the escalation delay, so a slow or stuck command
/// is debuggable.
///
/// Enabled only when stderr is a TTY and verbosity is 0 — under
/// `-v`/`-vv` the structured diagnostics take over, and a non-TTY (piped)
Expand Down Expand Up @@ -630,9 +632,9 @@ mod imp {
fn test_watchdog_block_status_only() {
// Before escalation (no command) the block is a single dim status
// row. A wide width leaves the status untruncated.
let block = watchdog_block("the commit message", 4, None, Some(200));
let block = watchdog_block("the commit generation command", 4, None, Some(200));
assert_eq!(block.len(), 1);
assert!(block[0].contains("Waiting for the commit message"));
assert!(block[0].contains("Waiting for the commit generation command"));
assert!(block[0].contains("(4s)"));
assert!(block[0].contains('↳'));
assert!(!block[0].contains('…'));
Expand All @@ -645,13 +647,13 @@ mod imp {
// command, since bash highlighting interleaves ANSI codes between
// tokens (a word itself is never split mid-token).
let block = watchdog_block(
"the commit message",
"the commit generation command",
12,
Some("claude --model=haiku"),
Some(200),
);
assert!(block.len() >= 2);
assert!(block[0].contains("Waiting for the commit message"));
assert!(block[0].contains("Waiting for the commit generation command"));
assert!(block.join("\n").contains("claude"));
}

Expand All @@ -662,7 +664,7 @@ mod imp {
// soft-wrapped second row desyncs the in-place cursor math.
use ansi_str::AnsiStr;
use unicode_width::UnicodeWidthStr;
let block = watchdog_block("the squash commit message", 1234, None, Some(20));
let block = watchdog_block("the commit generation command", 1234, None, Some(20));
assert_eq!(block.len(), 1);
let visible = block[0].ansi_strip();
assert!(UnicodeWidthStr::width(visible.as_ref()) <= 20);
Expand All @@ -672,15 +674,15 @@ mod imp {
#[test]
fn test_watchdog_start_with_non_tty_is_disabled() {
assert!(
Watchdog::start_with("the commit message", None, false)
Watchdog::start_with("the commit generation command", None, false)
.ticker
.is_none()
);
}

#[test]
fn test_watchdog_start_with_tty_is_enabled() {
let w = Watchdog::start_with("the commit message", None, true);
let w = Watchdog::start_with("the commit generation command", None, true);
assert!(w.ticker.is_some());
w.finish();
}
Expand All @@ -694,7 +696,7 @@ mod imp {
// far-off escalation delay keeps the gutter absent for the whole
// poll, so the `!escalated` assertion holds regardless of timing.
let w = Watchdog::enabled_with_delays(
"the commit message",
"the commit generation command",
Some("sh -c 'claude -p'"),
Duration::from_millis(10),
Duration::from_secs(3600),
Expand All @@ -720,7 +722,7 @@ mod imp {
// can't race the ticker into rendering under load; finish() wakes the
// parked thread immediately regardless.
let w = Watchdog::enabled_with_delays(
"the commit message",
"the commit generation command",
None,
Duration::from_secs(3600),
WATCHDOG_ESCALATE_DELAY,
Expand All @@ -739,7 +741,7 @@ mod imp {
// is what we're verifying. Generous timeout, fast poll — fast when
// healthy, reliable under load.
let w = Watchdog::enabled_with_delays(
"the commit message",
"the commit generation command",
Some("sh -c 'claude -p'"),
Duration::from_millis(10),
Duration::from_millis(30),
Expand Down Expand Up @@ -767,7 +769,7 @@ mod imp {
// load; escalation is gated on `command.is_some()`, so `!escalated`
// holds at any instant no matter how long the loop runs.
let w = Watchdog::enabled_with_delays(
"the commit message",
"the commit generation command",
None,
Duration::from_millis(10),
Duration::from_millis(30),
Expand Down
Loading