feat(tui): richer Work overflow and Agents hover detail in sidebar
- Work panel: hovering the '+N more checklist items' overflow row now reveals the omitted checklist items in the popover, so a height-constrained panel no longer hides work items with no way to inspect them (#3063). - Agents panel: the compact agent label row's hover text now carries a full dossier — id, role, status, elapsed time, step count, objective (new SidebarAgentRow field, from SubAgentAssignment), branch, and untruncated progress. Compact rows stay unchanged. - Detail-row hover keeps the full progress text instead of the summarized form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -427,11 +427,24 @@ fn work_panel_hover_texts(
|
||||
let later = summary.checklist_items.len().saturating_sub(end);
|
||||
let remaining = earlier.saturating_add(later);
|
||||
if remaining > 0 && texts.len() < max_rows {
|
||||
let label = match (earlier, later) {
|
||||
let mut label = match (earlier, later) {
|
||||
(0, later) => format!("+{later} more checklist items"),
|
||||
(earlier, 0) => format!("+{earlier} earlier checklist items"),
|
||||
(earlier, later) => format!("+{earlier} earlier, +{later} later"),
|
||||
};
|
||||
// Hovering the overflow row reveals the omitted items, since
|
||||
// the compact panel gives no other way to inspect them (#3063).
|
||||
let omitted = summary.checklist_items[..start]
|
||||
.iter()
|
||||
.chain(summary.checklist_items[end..].iter());
|
||||
for item in omitted {
|
||||
let prefix = match item.status {
|
||||
TodoStatus::Pending => "[ ]",
|
||||
TodoStatus::InProgress => "[~]",
|
||||
TodoStatus::Completed => "[✓]",
|
||||
};
|
||||
let _ = write!(label, "\n{prefix} #{} {}", item.id, item.content);
|
||||
}
|
||||
texts.push(label);
|
||||
}
|
||||
}
|
||||
@@ -1934,6 +1947,7 @@ pub struct SidebarAgentRow {
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub status: String,
|
||||
pub objective: Option<String>,
|
||||
pub git_branch: Option<String>,
|
||||
pub progress: Option<String>,
|
||||
pub steps_taken: u32,
|
||||
@@ -1984,6 +1998,8 @@ fn sidebar_agent_rows(app: &App) -> Vec<SidebarAgentRow> {
|
||||
name: display_name,
|
||||
role: agent.agent_type.as_str().to_string(),
|
||||
status: subagent_status_text(&agent.status).to_string(),
|
||||
objective: Some(agent.assignment.objective.clone())
|
||||
.filter(|objective| !objective.trim().is_empty()),
|
||||
git_branch: agent.git_branch.clone(),
|
||||
progress,
|
||||
steps_taken: agent.steps_taken,
|
||||
@@ -2013,6 +2029,7 @@ fn sidebar_agent_rows(app: &App) -> Vec<SidebarAgentRow> {
|
||||
name: display_name,
|
||||
role: "agent".to_string(),
|
||||
status: "running".to_string(),
|
||||
objective: None,
|
||||
git_branch: None,
|
||||
progress: Some(progress.clone()),
|
||||
steps_taken: 0,
|
||||
@@ -2230,8 +2247,10 @@ fn subagent_panel_hover_texts(
|
||||
if texts.len() >= max_rows {
|
||||
break;
|
||||
}
|
||||
let (marker, _) = agent_status_marker(row.status.as_str(), &palette::UI_THEME);
|
||||
texts.push(format!("{marker} {} {}", row.role, row.name));
|
||||
// The compact label row truncates aggressively, so its hover text
|
||||
// carries the full agent dossier: id, role, status, elapsed,
|
||||
// objective, branch, and untruncated progress (#3063).
|
||||
texts.push(agent_row_hover_text(row));
|
||||
|
||||
if row.status == "done" {
|
||||
continue;
|
||||
@@ -2248,7 +2267,7 @@ fn subagent_panel_hover_texts(
|
||||
if let Some(progress) = row.progress.as_deref()
|
||||
&& !progress.trim().is_empty()
|
||||
{
|
||||
detail_parts.push(summarize_tool_output(progress));
|
||||
detail_parts.push(progress.trim().to_string());
|
||||
}
|
||||
if let Some(branch) = row.git_branch.as_deref() {
|
||||
detail_parts.push(format!("branch {branch}"));
|
||||
@@ -2266,6 +2285,35 @@ fn subagent_panel_hover_texts(
|
||||
texts
|
||||
}
|
||||
|
||||
/// Full hover dossier for one Agents-panel label row (#3063). The compact
|
||||
/// row only shows `marker role name`, so hovering reveals everything else
|
||||
/// without spamming raw ids into the normal view.
|
||||
fn agent_row_hover_text(row: &SidebarAgentRow) -> String {
|
||||
let (marker, _) = agent_status_marker(row.status.as_str(), &palette::UI_THEME);
|
||||
let mut text = format!("{marker} {} {}", row.role, row.name);
|
||||
let _ = write!(text, "\nid: {}", row.id);
|
||||
let mut status_line = format!("status: {}", row.status);
|
||||
if let Some(duration) = row.duration_ms {
|
||||
let _ = write!(status_line, " · elapsed {}", format_duration_ms(duration));
|
||||
}
|
||||
if row.steps_taken > 0 {
|
||||
let _ = write!(status_line, " · {} step(s)", row.steps_taken);
|
||||
}
|
||||
let _ = write!(text, "\n{status_line}");
|
||||
if let Some(objective) = row.objective.as_deref() {
|
||||
let _ = write!(text, "\nobjective: {}", objective.trim());
|
||||
}
|
||||
if let Some(branch) = row.git_branch.as_deref() {
|
||||
let _ = write!(text, "\nbranch: {branch}");
|
||||
}
|
||||
if let Some(progress) = row.progress.as_deref()
|
||||
&& !progress.trim().is_empty()
|
||||
{
|
||||
let _ = write!(text, "\nprogress: {}", progress.trim());
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn agent_status_marker(
|
||||
status: &str,
|
||||
theme: &palette::UiTheme,
|
||||
@@ -2802,6 +2850,47 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_panel_overflow_hover_lists_omitted_checklist_items() {
|
||||
let summary = SidebarWorkSummary {
|
||||
checklist_completion_pct: 38,
|
||||
checklist_items: (1..=8)
|
||||
.map(|id| SidebarWorkChecklistItem {
|
||||
id,
|
||||
content: format!("Release task {id}"),
|
||||
status: if id <= 3 {
|
||||
TodoStatus::Completed
|
||||
} else if id == 5 {
|
||||
TodoStatus::InProgress
|
||||
} else {
|
||||
TodoStatus::Pending
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
..SidebarWorkSummary::default()
|
||||
};
|
||||
|
||||
let hover = work_panel_hover_texts(&summary, 80, 6);
|
||||
let overflow = hover
|
||||
.iter()
|
||||
.find(|text| text.starts_with('+'))
|
||||
.expect("overflow hover row should exist");
|
||||
|
||||
// Every checklist item is reachable: either as its own hover row or
|
||||
// listed inside the overflow row's hover text (#3063).
|
||||
for id in 1..=8 {
|
||||
let needle = format!("#{id} Release task {id}");
|
||||
assert!(
|
||||
hover.iter().any(|text| text.contains(&needle)),
|
||||
"item {id} should be inspectable via hover: {hover:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
overflow.lines().count() > 1,
|
||||
"overflow hover should enumerate omitted items: {overflow:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_panel_includes_strategy_only_when_plan_state_is_non_empty() {
|
||||
let empty_text = lines_to_text(&work_panel_lines(
|
||||
@@ -3372,6 +3461,7 @@ mod tests {
|
||||
name: "investigator".to_string(),
|
||||
role: "worker".to_string(),
|
||||
status: "running".to_string(),
|
||||
objective: None,
|
||||
git_branch: None,
|
||||
progress: Some("scanning".to_string()),
|
||||
steps_taken: 2,
|
||||
@@ -3409,6 +3499,7 @@ mod tests {
|
||||
name: "scout".to_string(),
|
||||
role: "explorer".to_string(),
|
||||
status: "running".to_string(),
|
||||
objective: None,
|
||||
git_branch: None,
|
||||
progress: Some("reading".to_string()),
|
||||
steps_taken: 1,
|
||||
@@ -3697,6 +3788,7 @@ mod tests {
|
||||
name: "check-docs-mcp".to_string(),
|
||||
role: "explore".to_string(),
|
||||
status: "running".to_string(),
|
||||
objective: None,
|
||||
git_branch: Some("feature/docs".to_string()),
|
||||
progress: Some("step 2/3: running tool 'read_file'".to_string()),
|
||||
steps_taken: 2,
|
||||
@@ -3707,6 +3799,7 @@ mod tests {
|
||||
name: "check-install-docs".to_string(),
|
||||
role: "general".to_string(),
|
||||
status: "done".to_string(),
|
||||
objective: None,
|
||||
git_branch: None,
|
||||
progress: Some("SUMMARY: docs checked".to_string()),
|
||||
steps_taken: 5,
|
||||
@@ -3961,6 +4054,7 @@ mod tests {
|
||||
name: "sidebar-detail-worker-with-long-name".to_string(),
|
||||
role: "worker".to_string(),
|
||||
status: "running".to_string(),
|
||||
objective: None,
|
||||
git_branch: Some("codex/sidebar-hover".to_string()),
|
||||
progress: Some(long_progress.to_string()),
|
||||
steps_taken: 9,
|
||||
@@ -3978,6 +4072,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_label_hover_carries_full_agent_dossier() {
|
||||
let mut role_counts = std::collections::BTreeMap::new();
|
||||
role_counts.insert("worker".to_string(), 1);
|
||||
let summary = SidebarSubagentSummary {
|
||||
cached_total: 1,
|
||||
cached_running: 1,
|
||||
role_counts,
|
||||
..SidebarSubagentSummary::default()
|
||||
};
|
||||
let rows = vec![SidebarAgentRow {
|
||||
id: "019e9142-83f6-7713-87f1-28902e74bf05".to_string(),
|
||||
name: "doc-checker".to_string(),
|
||||
role: "worker".to_string(),
|
||||
status: "running".to_string(),
|
||||
objective: Some("Verify install docs against the release notes".to_string()),
|
||||
git_branch: Some("codex/doc-check".to_string()),
|
||||
progress: Some("step 2/3: running tool 'read_file'".to_string()),
|
||||
steps_taken: 2,
|
||||
duration_ms: Some(22_000),
|
||||
}];
|
||||
|
||||
let hover = subagent_panel_hover_texts(&summary, &rows, 6);
|
||||
let label = hover
|
||||
.iter()
|
||||
.find(|text| text.contains("doc-checker"))
|
||||
.expect("label hover row should exist");
|
||||
|
||||
assert!(
|
||||
label.contains("id: 019e9142-83f6-7713-87f1-28902e74bf05"),
|
||||
"label hover should carry the full id: {label:?}"
|
||||
);
|
||||
assert!(
|
||||
label.contains("status: running") && label.contains("elapsed"),
|
||||
"label hover should carry status and elapsed time: {label:?}"
|
||||
);
|
||||
assert!(
|
||||
label.contains("objective: Verify install docs against the release notes"),
|
||||
"label hover should carry the objective: {label:?}"
|
||||
);
|
||||
assert!(
|
||||
label.contains("branch: codex/doc-check"),
|
||||
"label hover should carry the branch: {label:?}"
|
||||
);
|
||||
assert!(
|
||||
label.contains("progress: step 2/3: running tool 'read_file'"),
|
||||
"label hover should carry untruncated progress: {label:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── #3030: stable labels instead of raw internal ids ───────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user