From 4a6111edee0d689fb33489ac0c01d83ba45dc174 Mon Sep 17 00:00:00 2001 From: Zhuoran Deng Date: Thu, 28 May 2026 08:02:01 +0800 Subject: [PATCH 1/2] feat(tui): enrich activity detail context --- crates/tui/src/tui/ui.rs | 127 ++++++++++++++++++++++++++++----- crates/tui/src/tui/ui/tests.rs | 93 ++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 18 deletions(-) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index fb89de619..28f355795 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -7591,10 +7591,15 @@ fn activity_detail_text(app: &App, cell_index: usize, width: u16) -> Option Option 1 { + let previous_index = thinking_indices[position - 2]; + let preview = thinking_chunk_preview(app, previous_index); + sections.push(format!( + "Previous chunk: {} of {total} - {preview}", + position - 1 + )); + } + if position < total { + let next_index = thinking_indices[position]; + let preview = thinking_chunk_preview(app, next_index); + sections.push(format!( + "Next chunk: {} of {total} - {preview}", + position + 1 + )); + } } sections.push(String::new()); @@ -7685,6 +7706,18 @@ fn reasoning_timeline_text(app: &App, selected_cell_index: usize) -> Option String { + let Some(HistoryCell::Thinking { content, .. }) = app.cell_at_virtual_index(cell_index) else { + return "thinking".to_string(); + }; + let preview = one_line_summary(content, 64); + if preview.is_empty() { + "thinking".to_string() + } else { + preview + } +} + fn activity_cell_label(app: &App, cell_index: usize, cell: &HistoryCell) -> String { match cell { HistoryCell::Thinking { .. } => "thinking".to_string(), @@ -7793,30 +7826,88 @@ fn format_activity_duration_ms(ms: u64) -> String { } } -fn thinking_chunk_position(app: &App, cell_index: usize) -> Option<(usize, usize)> { - if !matches!( - app.cell_at_virtual_index(cell_index), - Some(HistoryCell::Thinking { .. }) - ) { - return None; - } - +fn activity_position(app: &App, cell_index: usize) -> Option<(usize, usize)> { let mut total = 0usize; let mut position = None; for idx in 0..app.virtual_cell_count() { - if matches!( - app.cell_at_virtual_index(idx), - Some(HistoryCell::Thinking { .. }) - ) { - total += 1; - if idx == cell_index { - position = Some(total); - } + let Some(cell) = app.cell_at_virtual_index(idx) else { + continue; + }; + if !is_meaningful_activity_cell(cell) { + continue; + } + total += 1; + if idx == cell_index { + position = Some(total); } } position.map(|pos| (pos, total)) } +fn activity_navigation_lines(app: &App, cell_index: usize) -> Vec { + let activity_indices: Vec = (0..app.virtual_cell_count()) + .filter(|&idx| { + app.cell_at_virtual_index(idx) + .is_some_and(is_meaningful_activity_cell) + }) + .collect(); + let Some(position) = activity_indices.iter().position(|&idx| idx == cell_index) else { + return Vec::new(); + }; + let total = activity_indices.len(); + let mut lines = Vec::new(); + if position > 0 { + let previous_idx = activity_indices[position - 1]; + if let Some(cell) = app.cell_at_virtual_index(previous_idx) { + let label = activity_cell_label(app, previous_idx, cell); + lines.push(format!( + "Previous activity: {} of {total} - {}", + position, + truncate_line_to_width(&label, 56) + )); + } + } + if position + 1 < total { + let next_idx = activity_indices[position + 1]; + if let Some(cell) = app.cell_at_virtual_index(next_idx) { + let label = activity_cell_label(app, next_idx, cell); + lines.push(format!( + "Next activity: {} of {total} - {}", + position + 2, + truncate_line_to_width(&label, 56) + )); + } + } + lines +} + +fn activity_detail_handle_line(app: &App, cell_index: usize, cell: &HistoryCell) -> Option { + if let Some(detail) = app.tool_detail_record_for_cell(cell_index) { + if let Some(artifact) = app + .session_artifacts + .iter() + .find(|artifact| artifact.tool_call_id == detail.tool_id) + { + return Some(format!( + "Detail handle: {} (retrieve_tool_result ref={}; Alt+V raw details)", + artifact.id, artifact.id + )); + } + return Some(format!( + "Detail handle: tool:{} (Alt+V raw details)", + detail.tool_id + )); + } + + match cell { + HistoryCell::Tool(_) if app.cell_has_detail_target(cell_index) => { + Some("Detail handle: Alt+V raw details".to_string()) + } + HistoryCell::SubAgent(_) => Some("Detail handle: Alt+V details".to_string()), + _ => None, + } +} + fn activity_cell_to_text(cell: &HistoryCell, width: u16) -> String { let lines = match cell { HistoryCell::Tool(_) => cell.lines_with_options( diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 4f0baa5bf..bd4bda1f3 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -5048,6 +5048,10 @@ fn activity_detail_opens_reasoning_timeline_for_selected_thinking() { body.contains("Selected chunk: 1 of 2"), "chunk position missing: {body}" ); + assert!( + body.contains("Next chunk: 2 of 2 - second chunk reasoning"), + "neighboring chunk missing: {body}" + ); assert!(body.contains("Thinking chunk 1 of 2 (selected)"), "{body}"); assert!(body.contains("Thinking chunk 2 of 2"), "{body}"); assert!(body.contains("first chunk reasoning"), "body: {body}"); @@ -5057,6 +5061,95 @@ fn activity_detail_opens_reasoning_timeline_for_selected_thinking() { ); } +#[test] +fn activity_detail_includes_tool_handle_and_neighbor_context() { + let mut app = create_test_app(); + app.history = vec![ + HistoryCell::Thinking { + content: "checked approach".to_string(), + streaming: false, + duration_secs: Some(0.6), + }, + HistoryCell::Tool(ToolCell::Generic(GenericToolCell { + name: "read_file".to_string(), + status: ToolStatus::Success, + input_summary: Some("src/main.rs".to_string()), + output: Some("bounded preview".to_string()), + prompts: None, + spillover_path: None, + output_summary: None, + is_diff: false, + })), + HistoryCell::Tool(ToolCell::Generic(GenericToolCell { + name: "grep_files".to_string(), + status: ToolStatus::Success, + input_summary: Some("TODO".to_string()), + output: Some("grep summary".to_string()), + prompts: None, + spillover_path: None, + output_summary: None, + is_diff: false, + })), + ]; + app.tool_details_by_cell.insert( + 1, + ToolDetailRecord { + tool_id: "call-read".to_string(), + tool_name: "read_file".to_string(), + input: serde_json::json!({"path": "src/main.rs"}), + output: Some("full output behind raw details".to_string()), + }, + ); + app.session_artifacts + .push(crate::artifacts::ArtifactRecord { + id: "art_call-read".to_string(), + kind: crate::artifacts::ArtifactKind::ToolOutput, + session_id: "session-activity".to_string(), + tool_call_id: "call-read".to_string(), + tool_name: "read_file".to_string(), + created_at: chrono::Utc::now(), + byte_size: 42, + preview: "bounded preview".to_string(), + storage_path: PathBuf::from("artifacts").join("art_call-read.txt"), + }); + app.resync_history_revisions(); + let revisions = app.history_revisions.clone(); + app.viewport.transcript_cache.ensure( + &app.history, + &revisions, + 100, + app.transcript_render_options(), + ); + let line = first_line_for_cell(&app, 1); + let point = TranscriptSelectionPoint { + line_index: line, + column: 0, + }; + app.viewport.transcript_selection.anchor = Some(point); + app.viewport.transcript_selection.head = Some(point); + + assert!(open_activity_detail_pager(&mut app)); + let body = pop_pager_body(&mut app); + + assert!(body.contains("Activity: read_file"), "{body}"); + assert!(body.contains("Activity chunk: 2 of 3"), "{body}"); + assert!( + body.contains("Previous activity: 1 of 3 - thinking"), + "{body}" + ); + assert!( + body.contains("Next activity: 3 of 3 - tool grep_files"), + "{body}" + ); + assert!(body.contains("Detail handle: art_call-read"), "{body}"); + assert!( + body.contains("retrieve_tool_result ref=art_call-read"), + "{body}" + ); + assert!(body.contains("Alt+V"), "{body}"); + assert!(body.contains("raw details"), "{body}"); +} + #[test] fn activity_detail_fallback_prefers_live_activity_context() { let mut app = create_test_app(); From 6d004597301fa6dccb76dd36112d612bd8371028 Mon Sep 17 00:00:00 2001 From: Zhuoran Deng Date: Thu, 28 May 2026 09:43:12 +0800 Subject: [PATCH 2/2] fix(tui): refine activity detail review feedback --- crates/tui/src/tui/ui.rs | 49 +++++++++++++--------------------- crates/tui/src/tui/ui/tests.rs | 5 ++++ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 28f355795..32dd1b633 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -7591,15 +7591,20 @@ fn activity_detail_text(app: &App, cell_index: usize, width: u16) -> Option String { } } -fn activity_position(app: &App, cell_index: usize) -> Option<(usize, usize)> { - let mut total = 0usize; - let mut position = None; - for idx in 0..app.virtual_cell_count() { - let Some(cell) = app.cell_at_virtual_index(idx) else { - continue; - }; - if !is_meaningful_activity_cell(cell) { - continue; - } - total += 1; - if idx == cell_index { - position = Some(total); - } - } - position.map(|pos| (pos, total)) -} - -fn activity_navigation_lines(app: &App, cell_index: usize) -> Vec { - let activity_indices: Vec = (0..app.virtual_cell_count()) +fn activity_indices(app: &App) -> Vec { + (0..app.virtual_cell_count()) .filter(|&idx| { app.cell_at_virtual_index(idx) .is_some_and(is_meaningful_activity_cell) }) - .collect(); - let Some(position) = activity_indices.iter().position(|&idx| idx == cell_index) else { - return Vec::new(); - }; + .collect() +} + +fn activity_navigation_lines( + app: &App, + position: usize, + activity_indices: &[usize], +) -> Vec { let total = activity_indices.len(); let mut lines = Vec::new(); if position > 0 { @@ -7900,9 +7891,7 @@ fn activity_detail_handle_line(app: &App, cell_index: usize, cell: &HistoryCell) } match cell { - HistoryCell::Tool(_) if app.cell_has_detail_target(cell_index) => { - Some("Detail handle: Alt+V raw details".to_string()) - } + HistoryCell::Tool(_) => Some("Detail handle: Alt+V details".to_string()), HistoryCell::SubAgent(_) => Some("Detail handle: Alt+V details".to_string()), _ => None, } diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index bd4bda1f3..9773cb031 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -5208,6 +5208,11 @@ fn activity_detail_fallback_uses_recent_meaningful_activity_without_full_tool_du body.contains("Alt+V for details"), "activity detail should stay bounded and point to Alt+V for raw detail: {body}" ); + assert!(body.contains("Detail handle: Alt+V details"), "{body}"); + assert!( + !body.contains("Detail handle: Alt+V raw details"), + "fallback tool details should not be labeled raw: {body}" + ); assert!( !body.contains("line 10"), "middle of large raw output should not be dumped into Activity Detail: {body}"