fix(tui): hide internal IDs from normal UI — stable labels for turns and agents (#3030)
Three changes to replace raw UUIDs/hex-ids with stable user-facing labels: 1. Turn label: Add turn_counter to App, display "Turn N" instead of the raw runtime_turn_id UUID prefix. Full UUID preserved in hover text. 2. Agent labels: Add agent_counter + agent_label_map to App. Populated on AgentSpawned; sidebar rows use "Agent 1", "Agent 2" etc. instead of agent_<hex>. Nicknames and user-assigned names still take priority. 3. Step counter: Add format_step_counter() helper. When max_steps is u32::MAX (the unbounded sentinel), renders "step 16" instead of the meaningless "step 16/4294967295". Concrete step budgets still show the denominator.
This commit is contained in:
@@ -70,6 +70,18 @@ fn release_resident_leases_for(agent_id: &str) {
|
||||
/// the `SubAgentManager`.
|
||||
const DEFAULT_MAX_STEPS: u32 = u32::MAX;
|
||||
const TOOL_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Format a step counter for sub-agent progress messages.
|
||||
///
|
||||
/// When `max_steps == u32::MAX` (the default), the denominator is a sentinel
|
||||
/// meaning "unbounded" — render just `step N` instead of `step N/4294967295`.
|
||||
fn format_step_counter(steps: u32, max_steps: u32) -> String {
|
||||
if max_steps == u32::MAX {
|
||||
format!("step {steps}")
|
||||
} else {
|
||||
format!("step {steps}/{max_steps}")
|
||||
}
|
||||
}
|
||||
// Non-streaming sub-agents need enough response budget to carry large tool-call
|
||||
// arguments, especially write_file content. The API bills generated tokens, not
|
||||
// the requested ceiling.
|
||||
@@ -4158,7 +4170,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: cancelled"),
|
||||
format!("{}: cancelled", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
if let Some(mb) = runtime.mailbox.as_ref() {
|
||||
let _ = mb.send(MailboxMessage::Cancelled {
|
||||
@@ -4210,7 +4222,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: requesting model response"),
|
||||
format!("{}: requesting model response", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
|
||||
while let Ok(input) = input_rx.try_recv() {
|
||||
@@ -4267,7 +4279,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: cancelled mid-request"),
|
||||
format!("{}: cancelled mid-request", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
if let Some(mb) = runtime.mailbox.as_ref() {
|
||||
let _ = mb.send(MailboxMessage::Cancelled {
|
||||
@@ -4330,7 +4342,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: interrupted; {reason}"),
|
||||
format!("{}: interrupted; {reason}", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
let status = SubAgentStatus::Interrupted(reason.clone());
|
||||
let duration_ms =
|
||||
@@ -4364,7 +4376,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: cancelled while interrupted"),
|
||||
format!("{}: cancelled while interrupted", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
if let Some(mb) = runtime.mailbox.as_ref() {
|
||||
let _ = mb.send(MailboxMessage::Cancelled {
|
||||
@@ -4488,7 +4500,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: {progress}"),
|
||||
format!("{}: {progress}", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
messages.push(Message {
|
||||
role: "user".to_string(),
|
||||
@@ -4524,7 +4536,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: complete"),
|
||||
format!("{}: complete", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -4534,17 +4546,18 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!(
|
||||
"step {steps}/{max_steps}: executing {} tool call(s)",
|
||||
tool_uses.len()
|
||||
),
|
||||
format!(
|
||||
"{}: executing {} tool call(s)",
|
||||
format_step_counter(steps, max_steps),
|
||||
tool_uses.len()
|
||||
),
|
||||
);
|
||||
let mut tool_results: Vec<ContentBlock> = Vec::new();
|
||||
for (tool_id, tool_name, tool_input) in tool_uses {
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: running tool '{tool_name}'"),
|
||||
format!("{}: running tool '{tool_name}'", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
if let Some(mb) = runtime.mailbox.as_ref() {
|
||||
let _ = mb.send(MailboxMessage::ToolCallStarted {
|
||||
@@ -4568,7 +4581,7 @@ async fn run_subagent(
|
||||
record_agent_progress(
|
||||
runtime,
|
||||
&agent_id,
|
||||
format!("step {steps}/{max_steps}: finished tool '{tool_name}'"),
|
||||
format!("{}: finished tool '{tool_name}'", format_step_counter(steps, max_steps)),
|
||||
);
|
||||
if let Some(mb) = runtime.mailbox.as_ref() {
|
||||
let _ = mb.send(MailboxMessage::ToolCallCompleted {
|
||||
|
||||
@@ -1445,6 +1445,13 @@ pub struct App {
|
||||
pub pending_subagent_dispatch: Option<String>,
|
||||
/// Animation anchor for status-strip active sub-agent spinner.
|
||||
pub agent_activity_started_at: Option<Instant>,
|
||||
/// Monotonic counter for stable agent labels (#3030).
|
||||
/// Incremented each time a sub-agent is spawned; used to generate
|
||||
/// "Agent 1", "Agent 2", etc.
|
||||
pub agent_counter: u64,
|
||||
/// Maps raw agent_id to a stable user-facing label (#3030).
|
||||
/// Populated when `AgentSpawned` fires; read by sidebar rendering.
|
||||
pub agent_label_map: HashMap<String, String>,
|
||||
pub ui_theme: UiTheme,
|
||||
/// Active named theme. Drives the cell-level color remap in
|
||||
/// `tui::color_compat::ColorCompatBackend` so community presets
|
||||
@@ -1628,6 +1635,9 @@ pub struct App {
|
||||
pub runtime_turn_id: Option<String>,
|
||||
/// Current runtime turn status (if known).
|
||||
pub runtime_turn_status: Option<String>,
|
||||
/// Monotonic turn counter for stable user-facing labels (#3030).
|
||||
/// Incremented each time a new turn starts; displayed as "Turn N".
|
||||
pub turn_counter: u64,
|
||||
/// When the UI accepted a user message but has not observed `TurnStarted` yet.
|
||||
pub dispatch_started_at: Option<Instant>,
|
||||
|
||||
@@ -2174,6 +2184,8 @@ impl App {
|
||||
last_fanout_card_index: None,
|
||||
pending_subagent_dispatch: None,
|
||||
agent_activity_started_at: None,
|
||||
agent_counter: 0,
|
||||
agent_label_map: HashMap::new(),
|
||||
ui_theme,
|
||||
theme_id,
|
||||
onboarding,
|
||||
@@ -2262,6 +2274,7 @@ impl App {
|
||||
last_balance_fetch: None,
|
||||
runtime_turn_id: None,
|
||||
runtime_turn_status: None,
|
||||
turn_counter: 0,
|
||||
dispatch_started_at: None,
|
||||
workspace_context: None,
|
||||
workspace_context_cell: std::sync::Arc::new(std::sync::Mutex::new(None)),
|
||||
|
||||
@@ -767,21 +767,22 @@ fn task_panel_lines(app: &App, content_width: usize, max_rows: usize) -> Vec<Lin
|
||||
let theme = &app.ui_theme;
|
||||
let mut lines: Vec<Line<'static>> = Vec::with_capacity(max_rows.max(4));
|
||||
|
||||
if let Some(turn_id) = app.runtime_turn_id.as_ref() {
|
||||
if app.runtime_turn_id.is_some() {
|
||||
let status = app
|
||||
.runtime_turn_status
|
||||
.as_deref()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
// Show enough of the turn id prefix to identify it for
|
||||
// task_read / task_cancel. A UUID needs ~13 chars before the
|
||||
// first hyphen; 16 chars gives a safe prefix for disambiguation.
|
||||
let turn_prefix = truncate_line_to_width(turn_id, 16);
|
||||
// #3030: Use a stable turn number ("Turn 1") instead of the raw
|
||||
// UUID prefix. The full UUID is preserved in the hover text
|
||||
// (task_panel_hover_texts) for inspection.
|
||||
let turn_label = if app.turn_counter > 0 {
|
||||
format!("Turn {} ({status})", app.turn_counter)
|
||||
} else {
|
||||
format!("Current turn ({status})")
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
truncate_line_to_width(
|
||||
&format!("turn {turn_prefix} ({status})",),
|
||||
content_width.max(1),
|
||||
),
|
||||
truncate_line_to_width(&turn_label, content_width.max(1)),
|
||||
Style::default().fg(theme.accent_primary),
|
||||
)));
|
||||
}
|
||||
@@ -1834,9 +1835,16 @@ fn sidebar_agent_rows(app: &App) -> Vec<SidebarAgentRow> {
|
||||
.map(summarize_tool_output)
|
||||
.filter(|summary| !summary.trim().is_empty())
|
||||
});
|
||||
// #3030: Prefer stable label ("Agent 1") > nickname > raw name.
|
||||
let display_name = app
|
||||
.agent_label_map
|
||||
.get(&agent.agent_id)
|
||||
.cloned()
|
||||
.or_else(|| agent.nickname.clone())
|
||||
.unwrap_or_else(|| agent.name.clone());
|
||||
SidebarAgentRow {
|
||||
id: agent.agent_id.clone(),
|
||||
name: agent.nickname.clone().unwrap_or_else(|| agent.name.clone()),
|
||||
name: display_name,
|
||||
role: agent.agent_type.as_str().to_string(),
|
||||
status: subagent_status_text(&agent.status).to_string(),
|
||||
git_branch: agent.git_branch.clone(),
|
||||
@@ -1856,17 +1864,25 @@ fn sidebar_agent_rows(app: &App) -> Vec<SidebarAgentRow> {
|
||||
app.agent_progress
|
||||
.iter()
|
||||
.filter(|(id, _)| !cached_ids.contains(id.as_str()))
|
||||
.map(|(id, progress)| SidebarAgentRow {
|
||||
id: id.clone(),
|
||||
name: id.clone(),
|
||||
role: "agent".to_string(),
|
||||
status: "running".to_string(),
|
||||
git_branch: None,
|
||||
progress: Some(progress.clone()),
|
||||
steps_taken: 0,
|
||||
duration_ms: app.agent_activity_started_at.map(|started| {
|
||||
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
|
||||
}),
|
||||
.map(|(id, progress)| {
|
||||
// #3030: Prefer stable label for progress-only agents too.
|
||||
let display_name = app
|
||||
.agent_label_map
|
||||
.get(id.as_str())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| id.clone());
|
||||
SidebarAgentRow {
|
||||
id: id.clone(),
|
||||
name: display_name,
|
||||
role: "agent".to_string(),
|
||||
status: "running".to_string(),
|
||||
git_branch: None,
|
||||
progress: Some(progress.clone()),
|
||||
steps_taken: 0,
|
||||
duration_ms: app.agent_activity_started_at.map(|started| {
|
||||
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1791,6 +1791,7 @@ async fn run_event_loop(
|
||||
}
|
||||
app.runtime_turn_id = Some(turn_id);
|
||||
app.runtime_turn_status = Some("in_progress".to_string());
|
||||
app.turn_counter = app.turn_counter.saturating_add(1);
|
||||
app.reasoning_buffer.clear();
|
||||
app.reasoning_header = None;
|
||||
app.last_reasoning = None;
|
||||
@@ -2301,6 +2302,12 @@ async fn run_event_loop(
|
||||
if app.agent_activity_started_at.is_none() {
|
||||
app.agent_activity_started_at = Some(Instant::now());
|
||||
}
|
||||
// #3030: Assign a stable user-facing label for this agent.
|
||||
if !app.agent_label_map.contains_key(&id) {
|
||||
app.agent_counter = app.agent_counter.saturating_add(1);
|
||||
app.agent_label_map
|
||||
.insert(id.clone(), format!("Agent {}", app.agent_counter));
|
||||
}
|
||||
app.status_message =
|
||||
Some(format!("Sub-agent {id} starting: {prompt_summary}"));
|
||||
let _ = engine_handle.send(Op::ListSubAgents).await;
|
||||
|
||||
Reference in New Issue
Block a user