diff --git a/src/commands/config/show.rs b/src/commands/config/show.rs index 9c9637052a..cd9a6bd77c 100644 --- a/src/commands/config/show.rs +++ b/src/commands/config/show.rs @@ -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 { + // 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") { @@ -1508,14 +1512,13 @@ fn fetch_latest_version() -> anyhow::Result { "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", diff --git a/src/commands/worktree/switch.rs b/src/commands/worktree/switch.rs index 89c074a75d..0402ee3de8 100644 --- a/src/commands/worktree/switch.rs +++ b/src/commands/worktree/switch.rs @@ -210,7 +210,7 @@ fn fetch_ref_info( repo: &Repository, ) -> anyhow::Result { 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) diff --git a/src/llm.rs b/src/llm.rs index e7e3c0602b..158030f207 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -100,9 +100,9 @@ pub(crate) fn render_llm_invocation(command: &str) -> anyhow::Result { /// ([`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. @@ -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, @@ -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(), @@ -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(), diff --git a/src/progress.rs b/src/progress.rs index 0256a56f73..9a371e10c6 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -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' /// ``` /// @@ -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) @@ -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('…')); @@ -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")); } @@ -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); @@ -672,7 +674,7 @@ 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() ); @@ -680,7 +682,7 @@ mod imp { #[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(); } @@ -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), @@ -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, @@ -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), @@ -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),