mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
wip
This commit is contained in:
@@ -47,6 +47,7 @@ pub(crate) use chat_composer::InputResult;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
|
||||
use crate::status_indicator_widget::StatusIndicatorWidget;
|
||||
pub(crate) use crate::status_indicator_widget::StatusSnapshot;
|
||||
pub(crate) use list_selection_view::SelectionAction;
|
||||
pub(crate) use list_selection_view::SelectionItem;
|
||||
|
||||
@@ -288,6 +289,14 @@ impl BottomPane {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_status_snapshot(&mut self, snapshot: StatusSnapshot) {
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
status.update_snapshot(snapshot);
|
||||
} else {
|
||||
self.update_status_header(snapshot.header);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show_ctrl_c_quit_hint(&mut self) {
|
||||
self.ctrl_c_quit_hint = true;
|
||||
self.composer
|
||||
|
||||
@@ -77,6 +77,7 @@ use crate::bottom_pane::SecurityReviewScopeConfirmView;
|
||||
use crate::bottom_pane::SelectionAction;
|
||||
use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::StatusSnapshot;
|
||||
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||
@@ -127,10 +128,13 @@ use codex_git_tooling::GhostCommit;
|
||||
use codex_git_tooling::GitToolingError;
|
||||
use codex_git_tooling::create_ghost_commit;
|
||||
use codex_git_tooling::restore_ghost_commit;
|
||||
use codex_protocol::plan_tool::StepStatus;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
const MAX_TRACKED_GHOST_COMMITS: usize = 20;
|
||||
const MAX_STATUS_THINKING_LINES: usize = 3;
|
||||
const MAX_STATUS_TOOL_CALLS: usize = 4;
|
||||
|
||||
// Track information about an in-flight exec command.
|
||||
struct RunningCommand {
|
||||
@@ -258,6 +262,9 @@ pub(crate) struct ChatWidget {
|
||||
full_reasoning_buffer: String,
|
||||
// Current status header shown in the status indicator.
|
||||
current_status_header: String,
|
||||
status_progress: Option<f32>,
|
||||
status_thinking_lines: Vec<String>,
|
||||
status_tool_calls: Vec<(String, String)>,
|
||||
// Previous status header to restore after a transient stream retry.
|
||||
retry_status_header: Option<String>,
|
||||
conversation_id: Option<ConversationId>,
|
||||
@@ -371,7 +378,88 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
self.current_status_header = header.clone();
|
||||
self.bottom_pane.update_status_header(header);
|
||||
if self.security_review_context.is_some() {
|
||||
self.bottom_pane.update_status_header(header);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_status_tracking(&mut self) {
|
||||
self.status_progress = None;
|
||||
self.status_thinking_lines.clear();
|
||||
self.status_tool_calls.clear();
|
||||
}
|
||||
|
||||
fn push_status_snapshot(&mut self) {
|
||||
if self.security_review_context.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tool_calls: Vec<String> = if self.status_tool_calls.len() <= MAX_STATUS_TOOL_CALLS {
|
||||
self.status_tool_calls
|
||||
.iter()
|
||||
.map(|(_, label)| label.clone())
|
||||
.collect()
|
||||
} else {
|
||||
self.status_tool_calls[self.status_tool_calls.len() - MAX_STATUS_TOOL_CALLS..]
|
||||
.iter()
|
||||
.map(|(_, label)| label.clone())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let snapshot = StatusSnapshot {
|
||||
header: self.current_status_header.clone(),
|
||||
progress: self.status_progress,
|
||||
thinking: self.status_thinking_lines.clone(),
|
||||
tool_calls,
|
||||
};
|
||||
self.bottom_pane.update_status_snapshot(snapshot);
|
||||
}
|
||||
|
||||
fn update_thinking_lines_from_reasoning(&mut self) {
|
||||
if self.security_review_context.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lines: Vec<String> = self
|
||||
.reasoning_buffer
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| truncate_text(line, 160))
|
||||
.collect();
|
||||
|
||||
if lines.len() > MAX_STATUS_THINKING_LINES {
|
||||
let start = lines.len() - MAX_STATUS_THINKING_LINES;
|
||||
lines = lines.split_off(start);
|
||||
}
|
||||
|
||||
self.status_thinking_lines = lines;
|
||||
}
|
||||
|
||||
fn add_tool_call(&mut self, key: String, label: String) {
|
||||
if self.security_review_context.is_some() {
|
||||
return;
|
||||
}
|
||||
self.status_tool_calls
|
||||
.retain(|(existing_key, _)| existing_key != &key);
|
||||
self.status_tool_calls.push((key, label));
|
||||
self.push_status_snapshot();
|
||||
}
|
||||
|
||||
fn remove_tool_call(&mut self, key: &str) {
|
||||
if self.security_review_context.is_some() {
|
||||
return;
|
||||
}
|
||||
let original_len = self.status_tool_calls.len();
|
||||
self.status_tool_calls
|
||||
.retain(|(existing_key, _)| existing_key != key);
|
||||
if self.status_tool_calls.len() != original_len {
|
||||
self.push_status_snapshot();
|
||||
}
|
||||
}
|
||||
|
||||
fn format_exec_command(command: &[String]) -> String {
|
||||
shlex::try_join(command.iter().map(String::as_str)).unwrap_or_else(|_| command.join(" "))
|
||||
}
|
||||
|
||||
// --- Small event handlers ---
|
||||
@@ -420,13 +508,13 @@ impl ChatWidget {
|
||||
// current reasoning block and extract the first bold element
|
||||
// (between **/**) as the chunk header. Show this header as status.
|
||||
self.reasoning_buffer.push_str(&delta);
|
||||
self.update_thinking_lines_from_reasoning();
|
||||
|
||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||
// Update the shimmer header to the extracted reasoning chunk header.
|
||||
self.set_status_header(header);
|
||||
} else {
|
||||
// Fallback while we don't yet have a bold header: leave existing header as-is.
|
||||
}
|
||||
self.push_status_snapshot();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -458,15 +546,19 @@ impl ChatWidget {
|
||||
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.retry_status_header = None;
|
||||
self.clear_status_tracking();
|
||||
self.set_status_header(String::from("Working"));
|
||||
self.full_reasoning_buffer.clear();
|
||||
self.reasoning_buffer.clear();
|
||||
self.push_status_snapshot();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_task_complete(&mut self, last_agent_message: Option<String>) {
|
||||
// If a stream is currently active, finalize it.
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.clear_status_tracking();
|
||||
self.push_status_snapshot();
|
||||
// Mark task stopped and request redraw now that all content is in history.
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.running_commands.clear();
|
||||
@@ -583,6 +675,26 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_plan_update(&mut self, update: UpdatePlanArgs) {
|
||||
if self.security_review_context.is_none() {
|
||||
if update.plan.is_empty() {
|
||||
self.status_progress = None;
|
||||
} else {
|
||||
let total = update.plan.len() as f32;
|
||||
let completed = update
|
||||
.plan
|
||||
.iter()
|
||||
.filter(|item| matches!(item.status, StepStatus::Completed))
|
||||
.count() as f32;
|
||||
let in_progress = update
|
||||
.plan
|
||||
.iter()
|
||||
.filter(|item| matches!(item.status, StepStatus::InProgress))
|
||||
.count() as f32;
|
||||
let progress = ((completed + in_progress * 0.5) / total).clamp(0.0, 1.0);
|
||||
self.status_progress = Some(progress);
|
||||
}
|
||||
self.push_status_snapshot();
|
||||
}
|
||||
self.add_to_history(history_cell::new_plan_update(update));
|
||||
}
|
||||
|
||||
@@ -656,12 +768,15 @@ impl ChatWidget {
|
||||
self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2));
|
||||
}
|
||||
|
||||
fn on_web_search_begin(&mut self, _ev: WebSearchBeginEvent) {
|
||||
fn on_web_search_begin(&mut self, ev: WebSearchBeginEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
let key = format!("web:{}", ev.call_id.as_str());
|
||||
self.add_tool_call(key, "Searching web".to_string());
|
||||
}
|
||||
|
||||
fn on_web_search_end(&mut self, ev: WebSearchEndEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.remove_tool_call(&format!("web:{}", ev.call_id.as_str()));
|
||||
self.add_to_history(history_cell::new_web_search_call(format!(
|
||||
"Searched: {}",
|
||||
ev.query
|
||||
@@ -698,6 +813,7 @@ impl ChatWidget {
|
||||
self.retry_status_header = Some(self.current_status_header.clone());
|
||||
}
|
||||
self.set_status_header(message);
|
||||
self.push_status_snapshot();
|
||||
}
|
||||
|
||||
/// Periodic tick to commit at most one queued line to history with a small delay,
|
||||
@@ -774,6 +890,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn handle_exec_end_now(&mut self, ev: ExecCommandEndEvent) {
|
||||
self.remove_tool_call(&format!("exec:{}", ev.call_id.as_str()));
|
||||
let running = self.running_commands.remove(&ev.call_id);
|
||||
let (command, parsed) = match running {
|
||||
Some(rc) => (rc.command, rc.parsed_cmd),
|
||||
@@ -864,6 +981,11 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) {
|
||||
// Ensure the status indicator is visible while the command runs.
|
||||
let command_display = Self::format_exec_command(&ev.command);
|
||||
let command_display = truncate_text(&command_display, 120);
|
||||
let label = format!("Running {command_display}");
|
||||
let key = format!("exec:{}", ev.call_id.as_str());
|
||||
self.add_tool_call(key, label);
|
||||
self.running_commands.insert(
|
||||
ev.call_id.clone(),
|
||||
RunningCommand {
|
||||
@@ -896,6 +1018,13 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
|
||||
let label_text = {
|
||||
let invocation = &ev.invocation;
|
||||
format!("Calling {}::{}", invocation.server, invocation.tool)
|
||||
};
|
||||
let label_text = truncate_text(&label_text, 120);
|
||||
let key = format!("mcp:{}", ev.call_id.as_str());
|
||||
self.add_tool_call(key, label_text);
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.flush_active_cell();
|
||||
self.active_cell = Some(Box::new(history_cell::new_active_mcp_tool_call(
|
||||
@@ -913,6 +1042,7 @@ impl ChatWidget {
|
||||
duration,
|
||||
result,
|
||||
} = ev;
|
||||
self.remove_tool_call(&format!("mcp:{}", call_id.as_str()));
|
||||
|
||||
let extra_cell = match self
|
||||
.active_cell
|
||||
@@ -1003,6 +1133,9 @@ impl ChatWidget {
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
current_status_header: String::from("Working"),
|
||||
status_progress: None,
|
||||
status_thinking_lines: Vec::new(),
|
||||
status_tool_calls: Vec::new(),
|
||||
retry_status_header: None,
|
||||
conversation_id: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -1071,6 +1204,9 @@ impl ChatWidget {
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
current_status_header: String::from("Working"),
|
||||
status_progress: None,
|
||||
status_thinking_lines: Vec::new(),
|
||||
status_tool_calls: Vec::new(),
|
||||
retry_status_header: None,
|
||||
conversation_id: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -2420,7 +2556,7 @@ impl ChatWidget {
|
||||
self.security_review_task = None;
|
||||
|
||||
let context = self.security_review_context.take();
|
||||
let (mode, scope_paths, output_root, repo_path, model, provider, started_at, last_log) =
|
||||
let (mode, scope_paths, _output_root, repo_path, model, provider, started_at, last_log) =
|
||||
if let Some(ctx) = context {
|
||||
(
|
||||
ctx.mode,
|
||||
|
||||
@@ -10,6 +10,7 @@ expression: term.backend().vt100().screen().contents()
|
||||
Read diff_render.rs
|
||||
|
||||
• Investigating rendering code (0s • esc to interrupt)
|
||||
↺ **Investigating rendering code**
|
||||
|
||||
|
||||
› Summarize recent commits
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 1577
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"• Analyzing (0s • esc to interrupt) "
|
||||
" ↺ **Analyzing** "
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
|
||||
@@ -277,6 +277,9 @@ fn make_chatwidget_manual() -> (
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
current_status_header: String::from("Working"),
|
||||
status_progress: None,
|
||||
status_thinking_lines: Vec::new(),
|
||||
status_tool_calls: Vec::new(),
|
||||
retry_status_header: None,
|
||||
conversation_id: None,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
|
||||
@@ -2953,7 +2953,7 @@ async fn generate_threat_model(
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let mut failure_logs = vec![
|
||||
let failure_logs = vec![
|
||||
"Threat model provider returned a response that could not be parsed.".to_string(),
|
||||
format!("Model error: {err}"),
|
||||
"Double-check API credentials and network availability for the security review process.".to_string(),
|
||||
@@ -2991,7 +2991,7 @@ async fn generate_threat_model(
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let mut failure_logs = vec![
|
||||
let failure_logs = vec![
|
||||
"Threat model retry still failed to decode the provider response.".to_string(),
|
||||
format!("Model error: {err}"),
|
||||
"Verify the provider is returning JSON (no HTML/proxy pages) and that credentials are correct.".to_string(),
|
||||
|
||||
@@ -20,9 +20,23 @@ use crate::key_hint;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
use crate::tui::FrameRequester;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StatusSnapshot {
|
||||
pub(crate) header: String,
|
||||
pub(crate) progress: Option<f32>,
|
||||
pub(crate) thinking: Vec<String>,
|
||||
pub(crate) tool_calls: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct StatusIndicatorWidget {
|
||||
/// Animated header text (defaults to "Working").
|
||||
header: String,
|
||||
/// Percentage progress to display, if available.
|
||||
progress: Option<f32>,
|
||||
/// Recent reasoning lines emitted by the model.
|
||||
thinking_lines: Vec<String>,
|
||||
/// Labels of in-flight tool calls.
|
||||
tool_calls: Vec<String>,
|
||||
/// Queued user messages to display under the status line.
|
||||
queued_messages: Vec<String>,
|
||||
|
||||
@@ -54,6 +68,9 @@ impl StatusIndicatorWidget {
|
||||
pub(crate) fn new(app_event_tx: AppEventSender, frame_requester: FrameRequester) -> Self {
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
progress: None,
|
||||
thinking_lines: Vec::new(),
|
||||
tool_calls: Vec::new(),
|
||||
queued_messages: Vec::new(),
|
||||
elapsed_running: Duration::ZERO,
|
||||
last_resume_at: Instant::now(),
|
||||
@@ -65,12 +82,14 @@ impl StatusIndicatorWidget {
|
||||
}
|
||||
|
||||
pub fn desired_height(&self, width: u16) -> u16 {
|
||||
// Status line + optional blank line + wrapped queued messages (up to 3 lines per message)
|
||||
// + optional ellipsis line per truncated message + 1 spacer line
|
||||
// Status line + optional thinking/tool call lines + optional blank line + wrapped queued messages
|
||||
// (up to 3 lines per message) + optional ellipsis per truncated message + keybind + spacer line
|
||||
let inner_width = width.max(1) as usize;
|
||||
let mut total: u16 = 1; // status line
|
||||
total = total.saturating_add(self.thinking_lines.len() as u16);
|
||||
total = total.saturating_add(self.tool_calls.len() as u16);
|
||||
if !self.queued_messages.is_empty() {
|
||||
total = total.saturating_add(1); // blank line between status and queued messages
|
||||
total = total.saturating_add(1); // blank line between supplemental and queued messages
|
||||
}
|
||||
let text_width = inner_width.saturating_sub(3); // account for " ↳ " prefix
|
||||
if text_width > 0 {
|
||||
@@ -108,6 +127,14 @@ impl StatusIndicatorWidget {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub(crate) fn update_snapshot(&mut self, snapshot: StatusSnapshot) {
|
||||
self.update_header(snapshot.header);
|
||||
self.progress = snapshot.progress;
|
||||
self.thinking_lines = snapshot.thinking;
|
||||
self.tool_calls = snapshot.tool_calls;
|
||||
self.frame_requester.schedule_frame();
|
||||
}
|
||||
|
||||
/// Replace the queued messages displayed beneath the header.
|
||||
pub(crate) fn set_queued_messages(&mut self, queued: Vec<String>) {
|
||||
self.queued_messages = queued;
|
||||
@@ -181,10 +208,21 @@ impl WidgetRef for StatusIndicatorWidget {
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to interrupt)".dim(),
|
||||
]);
|
||||
if let Some(progress) = self.progress {
|
||||
let pct = (progress.clamp(0.0, 1.0) * 100.0).round();
|
||||
spans.push(" ".into());
|
||||
spans.push(format!("{pct:.0}%").dim());
|
||||
}
|
||||
|
||||
// Build lines: status, then queued messages, then spacer.
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::from(spans));
|
||||
for thinking in &self.thinking_lines {
|
||||
lines.push(vec![" ↺ ".magenta(), thinking.clone().magenta()].into());
|
||||
}
|
||||
for call in &self.tool_calls {
|
||||
lines.push(vec![" ↳ ".cyan(), call.clone().cyan()].into());
|
||||
}
|
||||
if !self.queued_messages.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user