feat(cli): status display/filtering, expanded --fields, and robot-docs --brief
Work item status integration across all CLI output:
Issue listing (lore list issues):
- New Status column appears when any issue has status data, with
hex-color rendering using ANSI 256-color approximation
- New --status flag for case-insensitive filtering (OR logic for
multiple values): lore issues --status "In progress" --status "To do"
- Status fields (name, category, color, icon_name, synced_at) in issue
list query and JSON output with conditional serialization
Issue detail (lore show issue):
- Displays "Status: In progress (in_progress)" with color-coded output
using ANSI 256-color approximation from hex color values
- Status fields included in robot mode JSON with ISO timestamps
- IssueRow, IssueDetail, IssueDetailJson all carry status columns
Robot mode field selection expanded to new commands:
- search: --fields with "minimal" preset (document_id, title, source_type, score)
- timeline: --fields with "minimal" preset (timestamp, type, entity_iid, detail)
- who: --fields with per-mode presets (expert_minimal, workload_minimal, etc.)
- robot-docs: new --brief flag strips response_schema from output (~60% smaller)
- strip_schemas() utility in robot.rs for --brief mode
- expand_fields_preset() extended for search, timeline, and all who modes
Robot-docs manifest updated with --status flag documentation, --fields
flags for search/timeline/who, fields_presets sections, and corrected
search response schema field names.
Note: replaces empty commit dcfd449 which lost staging during hook execution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,29 @@ fn colored_cell(content: impl std::fmt::Display, color: Color) -> Cell {
|
||||
}
|
||||
}
|
||||
|
||||
fn colored_cell_hex(content: &str, hex: Option<&str>) -> Cell {
|
||||
if !console::colors_enabled() {
|
||||
return Cell::new(content);
|
||||
}
|
||||
let Some(hex) = hex else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
let hex = hex.trim_start_matches('#');
|
||||
if hex.len() != 6 {
|
||||
return Cell::new(content);
|
||||
}
|
||||
let Ok(r) = u8::from_str_radix(&hex[0..2], 16) else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
let Ok(g) = u8::from_str_radix(&hex[2..4], 16) else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
let Ok(b) = u8::from_str_radix(&hex[4..6], 16) else {
|
||||
return Cell::new(content);
|
||||
};
|
||||
Cell::new(content).fg(Color::Rgb { r, g, b })
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IssueListRow {
|
||||
pub iid: i64,
|
||||
@@ -34,6 +57,16 @@ pub struct IssueListRow {
|
||||
pub assignees: Vec<String>,
|
||||
pub discussion_count: i64,
|
||||
pub unresolved_count: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_category: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_icon_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_synced_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -51,6 +84,16 @@ pub struct IssueListRowJson {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub web_url: Option<String>,
|
||||
pub project_path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_category: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_icon_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_synced_at_iso: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&IssueListRow> for IssueListRowJson {
|
||||
@@ -68,6 +111,11 @@ impl From<&IssueListRow> for IssueListRowJson {
|
||||
updated_at_iso: ms_to_iso(row.updated_at),
|
||||
web_url: row.web_url.clone(),
|
||||
project_path: row.project_path.clone(),
|
||||
status_name: row.status_name.clone(),
|
||||
status_category: row.status_category.clone(),
|
||||
status_color: row.status_color.clone(),
|
||||
status_icon_name: row.status_icon_name.clone(),
|
||||
status_synced_at_iso: row.status_synced_at.map(ms_to_iso),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +242,7 @@ pub struct ListFilters<'a> {
|
||||
pub since: Option<&'a str>,
|
||||
pub due_before: Option<&'a str>,
|
||||
pub has_due_date: bool,
|
||||
pub statuses: &'a [String],
|
||||
pub sort: &'a str,
|
||||
pub order: &'a str,
|
||||
}
|
||||
@@ -291,6 +340,22 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
||||
where_clauses.push("i.due_date IS NOT NULL");
|
||||
}
|
||||
|
||||
let status_in_clause;
|
||||
if filters.statuses.len() == 1 {
|
||||
where_clauses.push("i.status_name = ? COLLATE NOCASE");
|
||||
params.push(Box::new(filters.statuses[0].clone()));
|
||||
} else if filters.statuses.len() > 1 {
|
||||
let placeholders: Vec<&str> = filters.statuses.iter().map(|_| "?").collect();
|
||||
status_in_clause = format!(
|
||||
"i.status_name COLLATE NOCASE IN ({})",
|
||||
placeholders.join(", ")
|
||||
);
|
||||
where_clauses.push(&status_in_clause);
|
||||
for s in filters.statuses {
|
||||
params.push(Box::new(s.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let where_sql = if where_clauses.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
@@ -338,7 +403,12 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
||||
(SELECT COUNT(*) FROM discussions d
|
||||
WHERE d.issue_id = i.id) AS discussion_count,
|
||||
(SELECT COUNT(*) FROM discussions d
|
||||
WHERE d.issue_id = i.id AND d.resolvable = 1 AND d.resolved = 0) AS unresolved_count
|
||||
WHERE d.issue_id = i.id AND d.resolvable = 1 AND d.resolved = 0) AS unresolved_count,
|
||||
i.status_name,
|
||||
i.status_category,
|
||||
i.status_color,
|
||||
i.status_icon_name,
|
||||
i.status_synced_at
|
||||
FROM issues i
|
||||
JOIN projects p ON i.project_id = p.id
|
||||
{where_sql}
|
||||
@@ -375,6 +445,11 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
||||
assignees,
|
||||
discussion_count: row.get(10)?,
|
||||
unresolved_count: row.get(11)?,
|
||||
status_name: row.get(12)?,
|
||||
status_category: row.get(13)?,
|
||||
status_color: row.get(14)?,
|
||||
status_icon_name: row.get(15)?,
|
||||
status_synced_at: row.get(16)?,
|
||||
})
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
@@ -683,18 +758,27 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
result.total_count
|
||||
);
|
||||
|
||||
let has_any_status = result.issues.iter().any(|i| i.status_name.is_some());
|
||||
|
||||
let mut header = vec![
|
||||
Cell::new("IID").add_attribute(Attribute::Bold),
|
||||
Cell::new("Title").add_attribute(Attribute::Bold),
|
||||
Cell::new("State").add_attribute(Attribute::Bold),
|
||||
];
|
||||
if has_any_status {
|
||||
header.push(Cell::new("Status").add_attribute(Attribute::Bold));
|
||||
}
|
||||
header.extend([
|
||||
Cell::new("Assignee").add_attribute(Attribute::Bold),
|
||||
Cell::new("Labels").add_attribute(Attribute::Bold),
|
||||
Cell::new("Disc").add_attribute(Attribute::Bold),
|
||||
Cell::new("Updated").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec![
|
||||
Cell::new("IID").add_attribute(Attribute::Bold),
|
||||
Cell::new("Title").add_attribute(Attribute::Bold),
|
||||
Cell::new("State").add_attribute(Attribute::Bold),
|
||||
Cell::new("Assignee").add_attribute(Attribute::Bold),
|
||||
Cell::new("Labels").add_attribute(Attribute::Bold),
|
||||
Cell::new("Disc").add_attribute(Attribute::Bold),
|
||||
Cell::new("Updated").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
.set_header(header);
|
||||
|
||||
for issue in &result.issues {
|
||||
let title = truncate_with_ellipsis(&issue.title, 45);
|
||||
@@ -709,15 +793,28 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
colored_cell(&issue.state, Color::DarkGrey)
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
let mut row = vec![
|
||||
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
||||
Cell::new(title),
|
||||
state_cell,
|
||||
];
|
||||
if has_any_status {
|
||||
match &issue.status_name {
|
||||
Some(status) => {
|
||||
row.push(colored_cell_hex(status, issue.status_color.as_deref()));
|
||||
}
|
||||
None => {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
row.extend([
|
||||
colored_cell(assignee, Color::Magenta),
|
||||
colored_cell(labels, Color::Yellow),
|
||||
Cell::new(discussions),
|
||||
colored_cell(relative_time, Color::DarkGrey),
|
||||
]);
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
println!("{table}");
|
||||
|
||||
Reference in New Issue
Block a user