From 789a0586d7d673723872f12ae41c4233207b751e Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:05:47 -0500 Subject: [PATCH] fix: add next-step hints to CLI errors and standardize empty states Upgrade terse, user-reachable bail! messages in coven-cli to the existing 'problem; next step' exemplar voice: empty prompt, patch --non-interactive requirements, empty issue text, empty interactive response, session-not-found (both local-store sites now point at `coven sessions --all`), daemon kill/input HTTP rejections (point at `coven daemon status` / `coven sessions`), and the Ward migration summary. Align the run 'nothing to do' bail with the same punctuation. Add explicit one-line empty states to `coven pc` disk/process sections (previously bare headers), and print a one-line note after sessions-search ingest warnings that fresh TUI transcripts may not be searchable until ingest completes. No control-flow, exit-code, or --json changes; the Cancelled no-Error-prefix contract is untouched. Closes #449 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Val Alexander <68980965+BunsDev@users.noreply.github.com> --- crates/coven-cli/src/main.rs | 50 +++++++++++++++++++++++------- crates/coven-cli/src/pc/display.rs | 12 +++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/crates/coven-cli/src/main.rs b/crates/coven-cli/src/main.rs index 56ba3f04..492fac8a 100644 --- a/crates/coven-cli/src/main.rs +++ b/crates/coven-cli/src/main.rs @@ -1100,6 +1100,7 @@ fn run_sessions_search(query: &str, json: bool) -> Result<()> { // `transcript_indexed_at` is set the ingest function is a no-op. let coven_home = coven_home_dir()?; let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true); + let mut ingest_warned = false; match store::list_uningest_external_sessions(&conn) { Ok(pending) => { for (session_id, _transcript_path) in pending { @@ -1111,14 +1112,22 @@ fn run_sessions_search(query: &str, json: bool) -> Result<()> { "warning: run_sessions_search: failed to ingest transcript for session \ {session_id}: {e}" ); + ingest_warned = true; } } } Err(e) => { // Non-fatal: fall through to search without transcript data. eprintln!("warning: run_sessions_search: failed to list un-ingested sessions: {e}"); + ingest_warned = true; } } + if ingest_warned { + eprintln!( + "note: fresh TUI session transcripts may not be searchable until ingest completes; \ + rerun the search in a moment" + ); + } let hits = store::search_events(&conn, query)?; @@ -2198,14 +2207,18 @@ fn run_patch( let git_state = openclaw_repo::inspect_git_state(&detected_repo.root)?; let issue = match joined_optional_issue(issue)? { Some(issue) => issue, - None if non_interactive => anyhow::bail!("issue text is required with --non-interactive"), + None if non_interactive => anyhow::bail!( + "issue text is required with --non-interactive; pass it as arguments, e.g. `coven patch openclaw \"fix the failing test\" --non-interactive --harness codex`" + ), None => { prompt_for_required_line(&format!("What is broken in {}? ", detected_repo.repo_name))? } }; let harness_id = match harness { Some(harness) => patch::HarnessId::parse(&harness)?, - None if non_interactive => anyhow::bail!("--harness is required with --non-interactive"), + None if non_interactive => anyhow::bail!( + "--harness is required with --non-interactive; add `--harness codex` (or claude, or copilot)" + ), None => choose_default_harness()?, }; let verification_profile = patch::VerificationProfile::parse(verify.as_deref())?; @@ -2313,7 +2326,9 @@ fn joined_optional_issue(issue: Vec) -> Result> { } let joined = issue.join(" ").trim().to_string(); if joined.is_empty() { - anyhow::bail!("issue text must not be empty when provided"); + anyhow::bail!( + "issue text must not be empty when provided; describe what is broken, e.g. `coven patch openclaw \"fix the failing gateway auth test\"`" + ); } Ok(Some(joined)) } @@ -2327,7 +2342,7 @@ fn prompt_for_required_line(prompt: &str) -> Result { .context("failed to read input")?; let line = line.trim().to_string(); if line.is_empty() { - anyhow::bail!("a response is required"); + anyhow::bail!("a response is required; type a non-empty answer, or press Ctrl-C to cancel"); } Ok(line) } @@ -2856,7 +2871,9 @@ fn run_ward_command(command: WardCommand) -> Result<()> { )?; ward_migrate::print_report(&report); if report.has_errors() { - bail!("one or more Ward migrations failed or were unmigratable"); + bail!( + "one or more Ward migrations failed or were unmigratable; see the report above for per-familiar details" + ); } } } @@ -3008,7 +3025,7 @@ fn run_session( }; if prompt_args.is_empty() && continue_session.is_none() { - anyhow::bail!("nothing to do: pass a prompt, or use --continue [ID] to resume a session"); + anyhow::bail!("nothing to do; pass a prompt, or use --continue [ID] to resume a session"); } let selected_harness = selected_available_harness(harness_id)?; @@ -3161,7 +3178,10 @@ fn run_session( r.updated_at = now.clone(); (r, true) } - None => anyhow::bail!("session {} not found in local store", id), + None => anyhow::bail!( + "session `{}` not found in local store; run `coven sessions --all` to list session ids", + id + ), } } else { let r = store::SessionRecord { @@ -3694,7 +3714,9 @@ pub(crate) fn summon_only_command(session_id: &str) -> Result Result<()> { "the daemon has no live process for session `{session_id}`; {}", STALE_RUNNING_HINT ), - status => anyhow::bail!("Coven daemon rejected the kill with HTTP {status}"), + status => anyhow::bail!( + "Coven daemon rejected the kill with HTTP {status}; check `coven daemon status` and `coven sessions` for session state" + ), } } @@ -3880,7 +3904,9 @@ fn send_session_input(coven_home: &Path, session_id: &str, data: &str) -> Result if (200..300).contains(&status) { Ok(()) } else { - anyhow::bail!("Coven daemon rejected input with HTTP {status}") + anyhow::bail!( + "Coven daemon rejected input with HTTP {status}; check `coven daemon status` and `coven sessions` for session state" + ) } } @@ -3959,7 +3985,9 @@ fn joined_prompt(prompt_args: &[String]) -> Result { let prompt = prompt_args.join(" "); let prompt = prompt.trim(); if prompt.is_empty() { - anyhow::bail!("prompt must not be empty"); + anyhow::bail!( + "prompt must not be empty; quote a task string, e.g. `coven run codex \"fix the failing tests\"`" + ); } Ok(prompt.to_string()) } diff --git a/crates/coven-cli/src/pc/display.rs b/crates/coven-cli/src/pc/display.rs index be4cb10f..06e1d106 100644 --- a/crates/coven-cli/src/pc/display.rs +++ b/crates/coven-cli/src/pc/display.rs @@ -76,11 +76,17 @@ fn print_human(snap: &SystemSnapshot, format: &OutputFormat) { println!(" ⏱ Uptime {}", format_uptime(snap.uptime_secs)); println!("\n── Disk ────────────────────────────────────"); + if snap.disks.is_empty() { + println!(" No disks detected."); + } for disk in &snap.disks { print_disk(disk); } println!("\n── Top Processes (by CPU) ──────────────────"); + if snap.processes.is_empty() { + println!(" No processes to show."); + } for proc in snap.processes.iter().take(10) { print_proc(proc, format); } @@ -115,6 +121,9 @@ pub fn print_top(snap: &SystemSnapshot, n: usize, format: &OutputFormat) { return; } println!("── Top {} Processes (by CPU) ────────────────", n); + if snap.processes.is_empty() { + println!(" No processes to show."); + } for proc in snap.processes.iter().take(n) { print_proc(proc, format); } @@ -126,6 +135,9 @@ pub fn print_disk_usage(snap: &SystemSnapshot, format: &OutputFormat) { return; } println!("── Disk Usage ──────────────────────────────"); + if snap.disks.is_empty() { + println!(" No disks detected."); + } for disk in &snap.disks { print_disk(disk); }