#[derive(Default, Clone)]
pub struct PanelData {
pub mcp: Vec<(String, bool)>,
pub lsp: Vec<(String, String, bool)>,
pub todos: Vec<(String, String)>,
pub modified: Vec<String>,
pub sysload: Option<crate::ui::sysload::SysLoadSnapshot>,
}
#[derive(Debug, Clone, Default)]
pub struct SubagentStatusRow {
pub id_short: String,
pub state: String,
pub prompt_short: String,
pub files: Vec<String>,
}
pub fn extract_file_paths_from_prompt(prompt: &str) -> Vec<String> {
let mut paths: Vec<String> = Vec::new();
for token in prompt.split_whitespace() {
let t = token.trim_matches(|c: char| c == ',' || c == ';' || c == ':');
if t.is_empty() || paths.len() >= 3 {
break;
}
if t.contains('/') || t.contains('\\') {
paths.push(t.to_string());
} else if let Some(idx) = t.rfind('.') {
let ext = &t[idx..];
if matches!(
ext,
".rs"
| ".py"
| ".ts"
| ".tsx"
| ".js"
| ".jsx"
| ".go"
| ".java"
| ".rb"
| ".c"
| ".cpp"
| ".h"
| ".hpp"
| ".clj"
| ".cljs"
| ".cljc"
| ".edn"
| ".toml"
| ".yaml"
| ".yml"
| ".json"
| ".md"
| ".txt"
| ".sh"
| ".bash"
) {
paths.push(t.to_string());
}
}
}
paths
}
#[derive(Debug, Clone, Default)]
pub struct ContextGauge {
pub used: u64,
pub window: u64,
pub pct: u16,
pub compactions: usize,
pub fold_soon: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GitSnapshot {
pub branch: String,
pub staged: usize,
pub unstaged: usize,
pub untracked: usize,
pub last_commit: String,
}
#[derive(Debug, Clone, Default)]
pub struct LeftPanelInfo {
pub context: ContextGauge,
pub activity: Vec<String>,
pub git: Option<GitSnapshot>,
}
pub fn tool_call_label(name: &str, args: &serde_json::Value) -> String {
let s = |k: &str| args.get(k).and_then(|v| v.as_str());
let basename = |p: &str| -> String { p.rsplit(['/', '\\']).next().unwrap_or(p).to_string() };
let clip = |v: &str, n: usize| -> String {
let one = v.split_whitespace().collect::<Vec<_>>().join(" ");
if one.chars().count() > n {
format!(
"{}…",
one.chars().take(n.saturating_sub(1)).collect::<String>()
)
} else {
one
}
};
let target = match name {
"read" | "write" | "edit" | "apply_patch" => s("path")
.or_else(|| s("file_path"))
.or_else(|| s("file"))
.map(basename),
"bash" => s("command").map(|c| clip(c, 28)),
"grep" => s("pattern").map(|p| clip(p, 24)),
"find_files" | "glob" => s("pattern").or_else(|| s("query")).map(|p| clip(p, 24)),
"list_dir" => s("path").map(basename),
"memory" | "skill" | "task" | "task_status" => s("name")
.or_else(|| s("action"))
.or_else(|| s("prompt"))
.map(|v| clip(v, 24)),
"bash_output" | "kill_shell" => s("id").map(|i| clip(i, 12)),
_ => None,
};
match target {
Some(t) if !t.is_empty() => format!("{name} {t}"),
_ => name.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::tool_call_label;
use serde_json::json;
#[test]
fn label_uses_basename_for_path_tools() {
assert_eq!(
tool_call_label("read", &json!({"path": "/abs/src/agent/run.rs"})),
"read run.rs"
);
assert_eq!(
tool_call_label("edit", &json!({"file_path": "src/ui/mod.rs"})),
"edit mod.rs"
);
}
#[test]
fn label_clips_bash_command_head() {
let out = tool_call_label(
"bash",
&json!({"command": "cargo test --all-features --workspace"}),
);
assert!(out.starts_with("bash cargo test"), "got: {out}");
assert!(out.chars().count() <= "bash ".len() + 28, "got: {out}");
}
#[test]
fn label_collapses_whitespace() {
let out = tool_call_label("bash", &json!({"command": "echo a\n b"}));
assert_eq!(out, "bash echo a b");
}
#[test]
fn label_falls_back_to_name_without_usable_args() {
assert_eq!(
tool_call_label("repo_overview", &json!({})),
"repo_overview"
);
assert_eq!(tool_call_label("read", &json!({})), "read");
}
}