hive-bash-mcp: shorten the running-task todo label instead of embedding the whole command
The todo pushed at task start embedded the entire raw shell command (task.cmd) as its summary — for a multi-line heredoc script (a common agent pattern), that balloons every UI that renders todo summaries to the command's full line count (hyperhive#3248). short_cmd_label keeps only the first non-blank line, char-truncated to 100 chars, with a trailing ellipsis whenever either the line itself was cut or more lines follow — so a short-looking first line ahead of a long heredoc body still reads as truncated, not as the complete command. The full command is still on disk in the task file for status/view; this only shortens the todo label.
This commit is contained in:
parent
36b2e2a16c
commit
31c34939ef
1 changed files with 63 additions and 2 deletions
|
|
@ -517,6 +517,34 @@ fn poll_once(socket: &Path, claimed: &Arc<Mutex<HashSet<String>>>) {
|
|||
// Task execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Max characters kept from the command's first line when building the
|
||||
/// "running" todo summary (see `short_cmd_label`).
|
||||
const CMD_LABEL_MAX_CHARS: usize = 100;
|
||||
|
||||
/// Build a short, single-line label for `cmd` to use in the "running"
|
||||
/// todo summary. `cmd` can be an entire multi-line script (heredocs are
|
||||
/// a common agent pattern) — embedding it whole balloons the todo body
|
||||
/// in every UI that renders it, so this keeps only the first non-blank
|
||||
/// line, char-truncated to `CMD_LABEL_MAX_CHARS`, with a trailing `…`
|
||||
/// whenever either the line itself was cut or more lines follow. The
|
||||
/// full command is still on disk in the task file for `status`/`view`.
|
||||
fn short_cmd_label(cmd: &str) -> String {
|
||||
let mut lines = cmd.lines().map(str::trim).filter(|l| !l.is_empty());
|
||||
let first = lines.next().unwrap_or("");
|
||||
let more_lines = lines.next().is_some();
|
||||
let char_truncated = first.chars().count() > CMD_LABEL_MAX_CHARS;
|
||||
let label: String = if char_truncated {
|
||||
first.chars().take(CMD_LABEL_MAX_CHARS).collect()
|
||||
} else {
|
||||
first.to_owned()
|
||||
};
|
||||
if char_truncated || more_lines {
|
||||
format!("{label}…")
|
||||
} else {
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_task(mut task: TaskFile, socket: &Path) {
|
||||
let id = task.id.clone();
|
||||
tracing::info!(id = %id, cmd = %task.cmd, "bash_runner: starting task");
|
||||
|
|
@ -529,7 +557,7 @@ async fn run_task(mut task: TaskFile, socket: &Path) {
|
|||
upsert_bash_todo(
|
||||
socket,
|
||||
&id,
|
||||
format!("bash task `{id}` running: `{}`", task.cmd),
|
||||
format!("bash task `{id}` running: `{}`", short_cmd_label(&task.cmd)),
|
||||
)
|
||||
.await;
|
||||
// Best-effort: tally the normalised command head for the /stats
|
||||
|
|
@ -830,10 +858,43 @@ fn done_summary(id: &str, summary: &str, output: Option<(bool, bool)>) -> String
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::validate_task_name;
|
||||
use super::{CMD_LABEL_MAX_CHARS, short_cmd_label, validate_task_name};
|
||||
use crate::test_util::with_harness_dir;
|
||||
use hive_types::Ident;
|
||||
|
||||
#[test]
|
||||
fn short_cmd_label_short_single_line_is_unchanged() {
|
||||
assert_eq!(short_cmd_label("echo hi"), "echo hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_cmd_label_trims_and_skips_leading_blank_lines() {
|
||||
assert_eq!(short_cmd_label("\n\n echo hi \nmore stuff"), "echo hi…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_cmd_label_flags_multiline_even_when_first_line_is_short() {
|
||||
// The whole point: a short first line followed by a long heredoc
|
||||
// body must still read as truncated, not as the complete command.
|
||||
let cmd = "cat > script.sh <<'SH'\n#!/usr/bin/env bash\necho hi\nSH\n";
|
||||
let label = short_cmd_label(cmd);
|
||||
assert_eq!(label, "cat > script.sh <<'SH'…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_cmd_label_char_truncates_a_long_single_line() {
|
||||
let long = "x".repeat(CMD_LABEL_MAX_CHARS + 50);
|
||||
let label = short_cmd_label(&long);
|
||||
assert_eq!(label.chars().count(), CMD_LABEL_MAX_CHARS + 1); // +1 for the `…`
|
||||
assert!(label.ends_with('…'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_cmd_label_handles_empty_command() {
|
||||
assert_eq!(short_cmd_label(""), "");
|
||||
assert_eq!(short_cmd_label(" \n \n"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_ident_names() {
|
||||
for ok in ["build", "ci-check", "t1", "task-123", "abc"] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue