Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 100 additions & 20 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7591,8 +7591,18 @@ fn activity_detail_text(app: &App, cell_index: usize, width: u16) -> Option<Stri
sections.push(status);
}

if let Some((position, total)) = thinking_chunk_position(app, cell_index) {
sections.push(format!("Thinking chunk: {position} of {total}"));
let activity_indices = activity_indices(app);
if let Some(position) = activity_indices.iter().position(|&idx| idx == cell_index) {
sections.push(format!(
"Activity chunk: {} of {}",
position + 1,
activity_indices.len()
));
sections.extend(activity_navigation_lines(app, position, &activity_indices));
}

if let Some(handle) = activity_detail_handle_line(app, cell_index, cell) {
sections.push(handle);
}

sections.push(String::new());
Expand Down Expand Up @@ -7644,6 +7654,22 @@ fn reasoning_timeline_text(app: &App, selected_cell_index: usize) -> Option<Stri
));
if let Some(position) = selected_position {
sections.push(format!("Selected chunk: {position} of {total}"));
if position > 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());

Expand Down Expand Up @@ -7685,6 +7711,18 @@ fn reasoning_timeline_text(app: &App, selected_cell_index: usize) -> Option<Stri
Some(sections.join("\n"))
}

fn thinking_chunk_preview(app: &App, cell_index: usize) -> 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(),
Expand Down Expand Up @@ -7793,28 +7831,70 @@ 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_indices(app: &App) -> Vec<usize> {
(0..app.virtual_cell_count())
.filter(|&idx| {
app.cell_at_virtual_index(idx)
.is_some_and(is_meaningful_activity_cell)
})
.collect()
}

fn activity_navigation_lines(
app: &App,
position: usize,
activity_indices: &[usize],
) -> Vec<String> {
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
}

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);
}
fn activity_detail_handle_line(app: &App, cell_index: usize, cell: &HistoryCell) -> Option<String> {
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(_) => Some("Detail handle: Alt+V details".to_string()),
HistoryCell::SubAgent(_) => Some("Detail handle: Alt+V details".to_string()),
_ => None,
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
position.map(|pos| (pos, total))
}

fn activity_cell_to_text(cell: &HistoryCell, width: u16) -> String {
Expand Down
98 changes: 98 additions & 0 deletions crates/tui/src/tui/ui/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand All @@ -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();
Expand Down Expand Up @@ -5115,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}"
Expand Down
Loading