#[derive(Debug, PartialEq, Eq)]
pub struct VerbPhrase {
pub verb: String,
pub detail: String,
}
static FRAGMENTS: &[(&str, &str, &str)] = &[
("compact", "compacting", "context"),
("compress", "compressing", "context"),
("summari", "summarizing", ""),
("recalling", "recalling", ""),
("loading memory", "loading", "memory"),
("memory store", "connecting", ""),
("memory", "searching", ""),
("recall", "searching", ""),
("matching skill", "matching", "skills"),
("syncing skill", "syncing", "skills"),
("rebuilding search", "rebuilding", "index"),
("reloading skill", "reloading", "skills"),
("loading skill", "loading", "skills"),
("skill", "loading", "skills"),
("index", "indexing", ""),
("mcp", "connecting", ""),
("working directory", "updating", "directory"),
("file-change hook", "running", "hook"),
("connecting tool", "connecting", "tools"),
("connecting", "connecting", ""),
("executing task", "executing", "task"),
("running", "running", ""),
("selecting tool", "selecting", "tools"),
("filtering tool", "filtering", "tools"),
("tool", "running", ""),
("shell", "running", ""),
("working", "working", ""),
("reflecting", "reflecting", ""),
("analyzing", "analyzing", ""),
("evaluating", "evaluating", ""),
("thinking", "thinking", ""),
("planning", "planning", ""),
("canceling", "canceling", ""),
("session digest", "generating", "digest"),
("generating recap", "generating", "recap"),
("generating session", "generating", "digest"),
("saving session", "saving", ""),
("loading conversation", "loading", "history"),
("shutting down", "shutting down", ""),
("waiting", "waiting", ""),
("loading", "loading", ""),
("retry", "retrying", ""),
("respond", "responding", ""),
("retrieve", "retrieving", ""),
("verify", "verifying", ""),
("stop", "stopping", ""),
];
#[must_use]
pub fn humanize(raw: &str) -> VerbPhrase {
let trimmed = raw.trim().trim_end_matches(['.', '…']).trim_end();
if trimmed.is_empty() {
return VerbPhrase {
verb: String::new(),
detail: String::new(),
};
}
let lower = trimmed.to_lowercase();
if let Some(rest) = lower.strip_prefix("retrying") {
let name = rest.trim_start_matches(|c: char| !c.is_alphabetic()).trim();
if !name.is_empty() {
return VerbPhrase {
verb: "retrying".into(),
detail: name.to_string(),
};
}
}
if let Some(rest) = lower.strip_prefix("utility action:")
&& let (Some(open), Some(close)) = (rest.rfind('('), rest.rfind(')'))
&& open < close
{
let verb_part = rest[..open].trim();
if let Some(phrase) = match_fragment(verb_part) {
return phrase;
}
let name = rest[open + 1..close].trim();
if let Some(phrase) = match_fragment(name) {
return phrase;
}
}
if let Some(phrase) = match_fragment(&lower) {
return phrase;
}
VerbPhrase {
verb: trimmed.to_string(),
detail: String::new(),
}
}
fn match_fragment(text: &str) -> Option<VerbPhrase> {
for &(fragment, verb, detail) in FRAGMENTS {
if text.contains(fragment) {
return Some(VerbPhrase {
verb: verb.into(),
detail: detail.into(),
});
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn vp(verb: &str, detail: &str) -> VerbPhrase {
VerbPhrase {
verb: verb.into(),
detail: detail.into(),
}
}
#[test]
fn compact_context() {
assert_eq!(
humanize("compacting context..."),
vp("compacting", "context")
);
assert_eq!(
humanize("soft compacting context..."),
vp("compacting", "context")
);
assert_eq!(
humanize("Compacting context (server-side)..."),
vp("compacting", "context")
);
}
#[test]
fn compress_context() {
assert_eq!(
humanize("compressing context..."),
vp("compressing", "context")
);
}
#[test]
fn summarizing() {
assert_eq!(humanize("summarizing..."), vp("summarizing", ""));
assert_eq!(humanize("summarizing output..."), vp("summarizing", ""));
}
#[test]
fn memory_recall() {
assert_eq!(humanize("recalling context..."), vp("recalling", ""));
assert_eq!(humanize("Loading memory..."), vp("loading", "memory"));
assert_eq!(
humanize("Connecting to memory store..."),
vp("connecting", "")
);
}
#[test]
fn skills() {
assert_eq!(humanize("matching skills..."), vp("matching", "skills"));
assert_eq!(humanize("matching skill..."), vp("matching", "skills"));
assert_eq!(humanize("Loading skills..."), vp("loading", "skills"));
assert_eq!(humanize("reloading skills..."), vp("reloading", "skills"));
assert_eq!(humanize("syncing skill index..."), vp("syncing", "skills"));
assert_eq!(
humanize("rebuilding search index..."),
vp("rebuilding", "index")
);
}
#[test]
fn indexing() {
assert_eq!(humanize("Indexing codebase..."), vp("indexing", ""));
}
#[test]
fn mcp() {
assert_eq!(
humanize("MCP server requesting input…"),
vp("connecting", "")
);
}
#[test]
fn tools_filtering() {
assert_eq!(humanize("filtering tools..."), vp("filtering", "tools"));
assert_eq!(humanize("selecting tools..."), vp("selecting", "tools"));
}
#[test]
fn thinking_and_evaluating() {
assert_eq!(humanize("thinking..."), vp("thinking", ""));
assert_eq!(humanize("Evaluating complexity..."), vp("evaluating", ""));
assert_eq!(humanize("Analyzing changes..."), vp("analyzing", ""));
}
#[test]
fn session_lifecycle() {
assert_eq!(
humanize("Generating session digest..."),
vp("generating", "digest")
);
assert_eq!(humanize("Generating recap..."), vp("generating", "recap"));
assert_eq!(humanize("Saving session summary..."), vp("saving", ""));
assert_eq!(
humanize("Loading conversation history..."),
vp("loading", "history")
);
assert_eq!(humanize("Shutting down..."), vp("shutting down", ""));
assert_eq!(humanize("Canceling plan..."), vp("canceling", ""));
}
#[test]
fn retrying_extracts_tool_name() {
let phrase = humanize("Retrying read...");
assert_eq!(phrase.verb, "retrying");
assert_eq!(phrase.detail, "read");
}
#[test]
fn utility_action_respond_shell() {
let phrase = humanize("Utility action: Respond (shell)");
assert_eq!(phrase.verb, "responding");
}
#[test]
fn utility_action_retrieve() {
let phrase = humanize("Utility action: Retrieve (some-tool)");
assert_eq!(phrase.verb, "retrieving");
}
#[test]
fn supervisor_label_passthrough() {
let phrase = humanize("mem-extract +2 more");
assert_eq!(phrase.verb, "mem-extract +2 more");
assert_eq!(phrase.detail, "");
}
#[test]
fn unknown_string_passthrough() {
let phrase = humanize("some unknown operation");
assert_eq!(phrase.verb, "some unknown operation");
assert_eq!(phrase.detail, "");
}
#[test]
fn empty_string_returns_empty() {
let phrase = humanize("");
assert_eq!(phrase.verb, "");
assert_eq!(phrase.detail, "");
}
#[test]
fn clear_status_returns_empty() {
let phrase = humanize(" ");
assert_eq!(phrase.verb, "");
assert_eq!(phrase.detail, "");
}
#[test]
fn trailing_dots_stripped() {
let a = humanize("thinking...");
let b = humanize("thinking");
assert_eq!(a, b);
}
#[test]
fn working_indicator() {
assert_eq!(humanize("working"), vp("working", ""));
assert_eq!(humanize("working..."), vp("working", ""));
assert_eq!(humanize("working\u{2026}"), vp("working", ""));
}
#[test]
fn connecting_tools() {
assert_eq!(humanize("Connecting tools..."), vp("connecting", "tools"));
assert_eq!(humanize("connecting..."), vp("connecting", ""));
}
#[test]
fn executing_task() {
let phrase = humanize("Executing task 1/3: some long task name...");
assert_eq!(phrase.verb, "executing");
assert_eq!(phrase.detail, "task");
}
#[test]
fn hooks_dispatch() {
assert_eq!(
humanize("Working directory changed\u{2026}"),
vp("updating", "directory")
);
assert_eq!(
humanize("Running file-change hook\u{2026}"),
vp("running", "hook")
);
}
#[test]
fn running_generic() {
assert_eq!(humanize("running..."), vp("running", ""));
}
}