v0.4.1 : Phase 1 complete (17/17) - dashboard TUI (#33), favoris/notes/tags (#39), profils d'environnement (#40), completions dynamiques + man pages (#41), packaging officiel + CI (#42), self-update Gitea, state v3, 212 tests
release / linux (push) Failing after 23s
release / windows (push) Canceled after 0s
release / macos (push) Canceled after 0s

This commit is contained in:
2026-08-17 10:50:13 -04:00
parent 4f4cd034b2
commit 56732d672b
93 changed files with 9412 additions and 1270 deletions
+55 -13
View File
@@ -13,31 +13,73 @@ struct SearchRow {
description: Option<String>,
website: Option<String>,
tags: Vec<String>,
/// Personal tags (issue #39).
personal_tags: Vec<String>,
favorite: bool,
}
pub fn run(app: &App, keyword: &str, category: Option<&str>) -> Result<i32> {
let hits = app.catalog.search(keyword, category);
pub fn run(app: &App, keyword: Option<&str>, category: Option<&str>, tag: Option<&str>) -> Result<i32> {
let keyword = keyword.unwrap_or("");
// An empty keyword (search --tag only) starts from the whole catalog.
let mut hits: Vec<&AgentDef> = if keyword.is_empty() {
app.catalog
.agents()
.iter()
.filter(|a| !a.hidden)
.filter(|a| category.map_or(true, |c| a.category.as_deref() == Some(c)))
.collect()
} else {
app.catalog.search(keyword, category)
};
// Personal tag filter (issue #39): keep agents carrying this tag.
if let Some(t) = tag {
let tagged = crate::commands::annotations_cmd::agents_with_tag(app, t);
if tagged.is_empty() {
app.log.info(&format!(
"no agent is tagged '{t}' — 'am tags' lists every personal tag"
));
return Ok(0);
}
hits.retain(|a| tagged.contains(&a.name));
}
if hits.is_empty() && tag.is_some() {
app.log.info(&format!(
"no agent matches the keyword and the tag '{}'",
tag.unwrap_or("")
));
return Ok(0);
}
if app.json() {
let rows: Vec<SearchRow> = hits
.iter()
.map(|a| SearchRow {
name: a.name.clone(),
display_name: a.title().to_string(),
category: a.category.clone(),
installable: a.installable,
description: a.description.clone(),
website: a.website.clone(),
tags: a.tags.clone(),
.map(|a| {
let personal = crate::commands::annotations_cmd::tags_of(app, &a.name);
SearchRow {
name: a.name.clone(),
display_name: a.title().to_string(),
category: a.category.clone(),
installable: a.installable,
description: a.description.clone(),
website: a.website.clone(),
tags: a.tags.clone(),
personal_tags: personal,
favorite: crate::commands::annotations_cmd::is_favorite(app, &a.name),
}
})
.collect();
print_json(&rows);
return Ok(0);
}
let mut table_rows = Vec::new();
for a in hits {
for a in &hits {
let desc = truncate(a.description.as_deref().unwrap_or(""), 56);
let star = if crate::commands::annotations_cmd::is_favorite(app, &a.name) {
"★ "
} else {
""
};
table_rows.push(vec![
a.title().to_string(),
format!("{star}{}", a.title()),
a.category.clone().unwrap_or_else(|| "—".to_string()),
if a.installable { "yes" } else { "no" }.to_string(),
desc,
@@ -52,7 +94,7 @@ pub fn run(app: &App, keyword: &str, category: Option<&str>) -> Result<i32> {
app.color(),
)
);
if app.catalog.search(keyword, category).is_empty() {
if hits.is_empty() && !keyword.is_empty() {
let suggestions = app.catalog.suggest(keyword);
if suggestions.is_empty() {
app.log