mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Show current plan step in TUI footer
This commit is contained in:
@@ -82,6 +82,7 @@ pub(crate) struct ChatComposer {
|
||||
attached_images: Vec<AttachedImage>,
|
||||
placeholder_text: String,
|
||||
is_task_running: bool,
|
||||
current_plan_step: Option<String>,
|
||||
// Non-bracketed paste burst tracker.
|
||||
paste_burst: PasteBurst,
|
||||
// When true, disables paste-burst logic and inserts characters immediately.
|
||||
@@ -127,6 +128,7 @@ impl ChatComposer {
|
||||
attached_images: Vec::new(),
|
||||
placeholder_text,
|
||||
is_task_running: false,
|
||||
current_plan_step: None,
|
||||
paste_burst: PasteBurst::default(),
|
||||
disable_paste_burst: false,
|
||||
custom_prompts: Vec::new(),
|
||||
@@ -177,6 +179,14 @@ impl ChatComposer {
|
||||
self.token_usage_info = token_info;
|
||||
}
|
||||
|
||||
pub(crate) fn set_current_plan_step(&mut self, step: Option<String>) -> bool {
|
||||
if self.current_plan_step == step {
|
||||
return false;
|
||||
}
|
||||
self.current_plan_step = step;
|
||||
true
|
||||
}
|
||||
|
||||
/// Record the history metadata advertised by `SessionConfiguredEvent` so
|
||||
/// that the composer can navigate cross-session history.
|
||||
pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) {
|
||||
@@ -1300,6 +1310,13 @@ impl WidgetRef for ChatComposer {
|
||||
hint.push(" edit prev".into());
|
||||
}
|
||||
|
||||
if let Some(step) = &self.current_plan_step {
|
||||
hint.push(" ".into());
|
||||
hint.push("Now:".dim());
|
||||
hint.push(" ".into());
|
||||
hint.push(step.clone().cyan().bold());
|
||||
}
|
||||
|
||||
// Append token/context usage info to the footer hints when available.
|
||||
if let Some(token_usage_info) = &self.token_usage_info {
|
||||
let token_usage = &token_usage_info.total_token_usage;
|
||||
@@ -1436,6 +1453,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footer_hint_includes_current_plan_step() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
assert!(composer.set_current_plan_step(Some("Implement feature".to_string())));
|
||||
|
||||
let area = Rect::new(0, 0, 80, 6);
|
||||
let mut buf = Buffer::empty(area);
|
||||
composer.render_ref(area, &mut buf);
|
||||
|
||||
let bottom_row: String = (0..area.width)
|
||||
.map(|x| {
|
||||
buf[(x, area.height - 1)]
|
||||
.symbol()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap_or(' ')
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
bottom_row.contains("Now:"),
|
||||
"missing status label: {bottom_row:?}"
|
||||
);
|
||||
assert!(
|
||||
bottom_row.contains("Implement feature"),
|
||||
"missing plan text: {bottom_row:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_current_at_token_basic_cases() {
|
||||
let test_cases = vec![
|
||||
|
||||
@@ -373,6 +373,12 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn set_plan_progress(&mut self, step: Option<String>) {
|
||||
if self.composer.set_current_plan_step(step) {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when the agent requests user approval.
|
||||
pub fn push_approval_request(&mut self, request: ApprovalRequest) {
|
||||
let request = if let Some(view) = self.active_view.as_mut() {
|
||||
|
||||
@@ -5,6 +5,8 @@ use std::sync::Arc;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_types::Notifications;
|
||||
use codex_core::plan_tool::PlanItemArg;
|
||||
use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
use codex_core::protocol::AgentReasoningDeltaEvent;
|
||||
@@ -172,6 +174,7 @@ impl ChatWidget {
|
||||
}
|
||||
// --- Small event handlers ---
|
||||
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
||||
self.bottom_pane.set_plan_progress(None);
|
||||
self.bottom_pane
|
||||
.set_history_metadata(event.history_log_id, event.history_entry_count);
|
||||
self.conversation_id = Some(event.session_id);
|
||||
@@ -325,6 +328,8 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_plan_update(&mut self, update: codex_core::plan_tool::UpdatePlanArgs) {
|
||||
let current_step = select_current_plan_step(&update.plan);
|
||||
self.bottom_pane.set_plan_progress(current_step);
|
||||
self.add_to_history(history_cell::new_plan_update(update));
|
||||
}
|
||||
|
||||
@@ -1560,5 +1565,37 @@ fn extract_first_bold(s: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn select_current_plan_step(plan: &[PlanItemArg]) -> Option<String> {
|
||||
let in_progress = plan.iter().find_map(|item| {
|
||||
if matches!(item.status, StepStatus::InProgress) {
|
||||
let trimmed = item.step.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if in_progress.is_some() {
|
||||
return in_progress;
|
||||
}
|
||||
|
||||
plan.iter().find_map(|item| {
|
||||
if matches!(item.status, StepStatus::Pending) {
|
||||
let trimmed = item.step.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
|
||||
Reference in New Issue
Block a user