fix(tui/explore): keep Exploring header until non-reasoning cell

This commit is contained in:
James Peter
2025-10-03 16:04:20 +10:00
parent 45293761bb
commit 704879312f
8 changed files with 139 additions and 64 deletions
+64 -53
View File
@@ -88,8 +88,8 @@ use self::auto_coordinator::{
use self::limits_overlay::{LimitsOverlay, LimitsOverlayContent, LimitsTab};
use self::rate_limit_refresh::start_rate_limit_refresh;
use self::history_render::{
CachedLayout, HistoryRenderState, RenderRequest, RenderRequestKind, RenderSettings,
VisibleCell,
explore_should_hold_title, CachedLayout, HistoryRenderState, RenderRequest, RenderRequestKind,
RenderSettings, VisibleCell,
};
use codex_core::parse_command::ParsedCommand;
use codex_core::protocol::AgentMessageDeltaEvent;
@@ -2514,7 +2514,7 @@ impl ChatWidget<'_> {
fn refresh_reasoning_collapsed_visibility(&mut self) {
let show = self.config.tui.show_reasoning;
let mut changed = false;
let mut needs_invalidate = false;
if show {
for cell in &self.history_cells {
if let Some(reasoning_cell) = cell
@@ -2522,70 +2522,78 @@ impl ChatWidget<'_> {
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
{
if reasoning_cell.set_hide_when_collapsed(false) {
changed = true;
needs_invalidate = true;
}
}
}
if changed {
self.invalidate_height_cache();
}
return;
}
use std::collections::HashSet;
let mut hide_indices: HashSet<usize> = HashSet::new();
let len = self.history_cells.len();
let mut idx = 0usize;
while idx < len {
let is_explore = self.history_cells[idx]
.as_any()
.downcast_ref::<history_cell::ExploreAggregationCell>()
.is_some();
if !is_explore {
idx += 1;
continue;
}
let mut reasoning_indices: Vec<usize> = Vec::new();
let mut j = idx + 1;
while j < len {
if self.history_cells[j]
} else {
use std::collections::HashSet;
let mut hide_indices: HashSet<usize> = HashSet::new();
let len = self.history_cells.len();
let mut idx = 0usize;
while idx < len {
let is_explore = self.history_cells[idx]
.as_any()
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
.is_some()
{
reasoning_indices.push(j);
j += 1;
.downcast_ref::<history_cell::ExploreAggregationCell>()
.is_some();
if !is_explore {
idx += 1;
continue;
}
break;
let mut reasoning_indices: Vec<usize> = Vec::new();
let mut j = idx + 1;
while j < len {
if self.history_cells[j]
.as_any()
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
.is_some()
{
reasoning_indices.push(j);
j += 1;
continue;
}
break;
}
if reasoning_indices.len() > 1 {
for &ri in &reasoning_indices[..reasoning_indices.len() - 1] {
hide_indices.insert(ri);
}
}
idx = j;
}
if reasoning_indices.len() > 1 {
for &ri in &reasoning_indices[..reasoning_indices.len() - 1] {
hide_indices.insert(ri);
for (i, cell) in self.history_cells.iter().enumerate() {
if let Some(reasoning_cell) = cell
.as_any()
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
{
if hide_indices.contains(&i) {
if reasoning_cell.set_hide_when_collapsed(true) {
needs_invalidate = true;
}
} else if reasoning_cell.set_hide_when_collapsed(false) {
needs_invalidate = true;
}
}
}
idx = j;
}
for (i, cell) in self.history_cells.iter().enumerate() {
if let Some(reasoning_cell) = cell
.as_any()
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
for cell in &mut self.history_cells {
if let Some(explore_cell) = cell
.as_any_mut()
.downcast_mut::<history_cell::ExploreAggregationCell>()
{
if hide_indices.contains(&i) {
if reasoning_cell.set_hide_when_collapsed(true) {
changed = true;
}
} else {
if reasoning_cell.set_hide_when_collapsed(false) {
changed = true;
}
let should_force =
explore_should_hold_title(&self.history_state, explore_cell.record().id);
if explore_cell.set_force_exploring_header(should_force) {
needs_invalidate = true;
}
}
}
if changed {
if needs_invalidate {
self.invalidate_height_cache();
self.request_redraw();
}
}
@@ -5879,9 +5887,12 @@ impl ChatWidget<'_> {
HistoryRecord::Patch(state) => {
Some(Box::new(history_cell::PatchSummaryCell::from_record(state.clone())))
}
HistoryRecord::Explore(state) => Some(Box::new(
history_cell::ExploreAggregationCell::from_record(state.clone()),
)),
HistoryRecord::Explore(state) => {
let mut cell = history_cell::ExploreAggregationCell::from_record(state.clone());
let hold_title = explore_should_hold_title(&self.history_state, state.id);
cell.set_force_exploring_header(hold_title);
Some(Box::new(cell))
}
HistoryRecord::RateLimits(state) => Some(Box::new(
history_cell::RateLimitsCell::from_record(state.clone()),
)),
+44 -4
View File
@@ -10,7 +10,7 @@ use crate::history::state::{HistoryId, HistoryRecord, HistoryState};
use crate::history_cell::{
assistant_markdown_lines,
compute_assistant_layout,
explore_lines_from_record,
explore_lines_from_record_with_force,
diff_lines_from_record,
exec_display_lines_from_record,
merged_exec_lines_from_record,
@@ -446,7 +446,8 @@ impl<'a> RenderRequest<'a> {
if let RenderRequestKind::Explore { id } = self.kind {
if let Some(HistoryRecord::Explore(record)) = history_state.record(id) {
return explore_lines_from_record(record);
let hold_title = explore_should_hold_title(history_state, id);
return explore_lines_from_record_with_force(record, hold_title);
}
}
@@ -493,6 +494,29 @@ pub(crate) enum RenderRequestKind {
Assistant { id: HistoryId },
}
pub(crate) fn explore_should_hold_title(history_state: &HistoryState, explore_id: HistoryId) -> bool {
if explore_id == HistoryId::ZERO {
return true;
}
let Some(mut idx) = history_state.index_of(explore_id) else {
return true;
};
idx += 1;
while let Some(record) = history_state.get(idx) {
match record {
HistoryRecord::Reasoning(_) => {
idx += 1;
continue;
}
_ => return false,
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1254,7 +1278,7 @@ mod tests {
.expect("explore layout missing")
.layout();
let text = collect_lines(&layout);
assert!(text.iter().any(|line| line.contains("Explored")));
assert!(text.iter().any(|line| line.contains("Exploring")));
assert!(text.iter().any(|line| line.contains("pattern")));
}
@@ -1370,8 +1394,24 @@ mod tests {
"collapsed reasoning cells should not push the next visible cell"
);
}
}
#[test]
fn explore_should_hold_title_until_non_reasoning_block() {
let mut state = HistoryState::new();
let explore_id = insert_explore_record(&mut state);
assert!(explore_should_hold_title(&state, explore_id));
let _reasoning_id = state.push(HistoryRecord::Reasoning(make_reasoning_state(
"reasoning only",
)));
assert!(explore_should_hold_title(&state, explore_id));
let _plain_id = state.push(HistoryRecord::PlainMessage(make_plain_message(
"ready to summarize",
)));
assert!(!explore_should_hold_title(&state, explore_id));
}
}
/// Output from `HistoryRenderState::visible_cells()`. Contains the resolved
/// layout (if any), plus the optional `HistoryCell` pointer so the caller can
/// reuse existing caches.
@@ -2,5 +2,5 @@
source: tui/src/chatwidget/tests.rs
expression: blob2
---
• Explored
• Exploring...
└ List ls -la
@@ -2,6 +2,6 @@
source: tui/src/chatwidget/tests.rs
expression: blob4
---
• Explored
• Exploring...
└ List ls -la
Read foo.txt
@@ -2,6 +2,6 @@
source: tui/src/chatwidget/tests.rs
expression: blob5
---
• Explored
• Exploring...
└ List ls -la
Read foo.txt
@@ -2,6 +2,6 @@
source: tui/src/chatwidget/tests.rs
expression: blob6
---
• Explored
• Exploring...
└ List ls -la
Read foo.txt, bar.txt
+26 -3
View File
@@ -13,11 +13,15 @@ use std::path::{Component, Path};
pub(crate) struct ExploreAggregationCell {
record: ExploreRecord,
force_exploring_header: bool,
}
impl ExploreAggregationCell {
pub(crate) fn from_record(record: ExploreRecord) -> Self {
Self { record }
Self {
record,
force_exploring_header: false,
}
}
pub(crate) fn record(&self) -> &ExploreRecord {
@@ -28,6 +32,14 @@ impl ExploreAggregationCell {
&mut self.record
}
pub(crate) fn set_force_exploring_header(&mut self, force: bool) -> bool {
if self.force_exploring_header == force {
return false;
}
self.force_exploring_header = force;
true
}
fn current_exec_status(&self) -> ExecStatus {
if self
.record
@@ -66,7 +78,7 @@ impl HistoryCell for ExploreAggregationCell {
}
fn display_lines(&self) -> Vec<Line<'static>> {
explore_lines_from_record(&self.record)
explore_lines_from_record_with_force(&self.record, self.force_exploring_header)
}
fn desired_height(&self, width: u16) -> u16 {
@@ -255,7 +267,15 @@ pub(crate) fn explore_record_update_status(
}
pub(crate) fn explore_lines_from_record(record: &ExploreRecord) -> Vec<Line<'static>> {
let exploring = record.entries.is_empty()
explore_lines_from_record_with_force(record, false)
}
pub(crate) fn explore_lines_from_record_with_force(
record: &ExploreRecord,
force_exploring: bool,
) -> Vec<Line<'static>> {
let exploring = force_exploring
|| record.entries.is_empty()
|| record
.entries
.iter()
@@ -375,6 +395,9 @@ mod tests {
}
let completed_lines = explore_lines_from_record(&completed);
assert_eq!(line_text(&completed_lines[0]), "Explored");
let forced_lines = explore_lines_from_record_with_force(&completed, true);
assert_eq!(line_text(&forced_lines[0]), "Exploring...");
}
#[test]
+1
View File
@@ -111,6 +111,7 @@ pub(crate) use diff::{
};
pub(crate) use explore::{
explore_lines_from_record,
explore_lines_from_record_with_force,
explore_record_push_from_parsed,
explore_record_update_status,
ExploreAggregationCell,