mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
wip
This commit is contained in:
@@ -380,7 +380,10 @@ impl ChatWidget<'_> {
|
||||
cwd: cwd.clone(),
|
||||
},
|
||||
);
|
||||
self.active_history_cell = Some(HistoryCell::new_active_exec_command(command));
|
||||
self.active_history_cell = Some(HistoryCell::new_active_exec_command(
|
||||
command,
|
||||
self.app_event_tx.clone(),
|
||||
));
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(_) => {
|
||||
// TODO
|
||||
|
||||
@@ -31,9 +31,16 @@ use ratatui::widgets::Wrap;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use tracing::error;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
pub(crate) struct CommandOutput {
|
||||
pub(crate) exit_code: i32,
|
||||
pub(crate) stdout: String,
|
||||
@@ -78,7 +85,7 @@ pub(crate) enum HistoryCell {
|
||||
AgentReasoning { view: TextBlock },
|
||||
|
||||
/// An exec tool call that has not finished yet.
|
||||
ActiveExecCommand { command: String },
|
||||
ActiveExecCommand { view: ActiveExecCommandView },
|
||||
|
||||
/// Completed exec tool call.
|
||||
CompletedExecCommand { view: TextBlock },
|
||||
@@ -148,10 +155,10 @@ impl HistoryCell {
|
||||
| HistoryCell::ActiveMcpToolCall { view, .. } => {
|
||||
view.lines.iter().map(line_to_static).collect()
|
||||
}
|
||||
HistoryCell::ActiveExecCommand { command, .. } => {
|
||||
HistoryCell::ActiveExecCommand { view, .. } => {
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
Line::from(vec!["command".magenta(), " running...".dim()]),
|
||||
Line::from(format!("$ {command}")),
|
||||
Line::from(format!("$ {}", view.command)),
|
||||
Line::from(""),
|
||||
];
|
||||
lines.iter().map(line_to_static).collect()
|
||||
@@ -178,9 +185,7 @@ impl HistoryCell {
|
||||
| HistoryCell::PendingPatch { view }
|
||||
| HistoryCell::PlanUpdate { view }
|
||||
| HistoryCell::ActiveMcpToolCall { view, .. } => Box::new(view),
|
||||
HistoryCell::ActiveExecCommand { command, .. } => Box::new(ActiveExecCommandView {
|
||||
command: command.clone(),
|
||||
}),
|
||||
HistoryCell::ActiveExecCommand { view, .. } => Box::new(view),
|
||||
HistoryCell::CompletedMcpToolCallWithImageOutput { .. } => {
|
||||
panic!("view() called on image output cell")
|
||||
}
|
||||
@@ -292,11 +297,14 @@ impl HistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_active_exec_command(command: Vec<String>) -> Self {
|
||||
pub(crate) fn new_active_exec_command(
|
||||
command: Vec<String>,
|
||||
app_event_tx: AppEventSender,
|
||||
) -> Self {
|
||||
let command_escaped = strip_bash_lc_and_escape(&command);
|
||||
|
||||
HistoryCell::ActiveExecCommand {
|
||||
command: command_escaped,
|
||||
view: ActiveExecCommandView::new(command_escaped, app_event_tx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,13 +684,106 @@ impl WidgetRef for &HistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveExecCommandView {
|
||||
pub(crate) struct ActiveExecCommandView {
|
||||
command: String,
|
||||
frame_idx: Arc<AtomicUsize>,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
impl DynamicHeightWidgetRef for ActiveExecCommandView {
|
||||
|
||||
impl ActiveExecCommandView {
|
||||
fn new(command: String, app_event_tx: AppEventSender) -> Self {
|
||||
let frame_idx = Arc::new(AtomicUsize::new(0));
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Animation thread to drive shimmer and trigger redraws.
|
||||
{
|
||||
let frame_idx_clone = Arc::clone(&frame_idx);
|
||||
let running_clone = Arc::clone(&running);
|
||||
let app_event_tx_clone = app_event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut counter = 0usize;
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
counter = counter.wrapping_add(1);
|
||||
frame_idx_clone.store(counter, Ordering::Relaxed);
|
||||
app_event_tx_clone.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
command,
|
||||
frame_idx,
|
||||
running,
|
||||
}
|
||||
}
|
||||
|
||||
fn shimmering_header_spans(&self) -> Vec<Span<'static>> {
|
||||
let header_text = "command";
|
||||
let header_chars: Vec<char> = header_text.chars().collect();
|
||||
|
||||
let padding = 4usize; // virtual padding around the word for smoother loop
|
||||
let period = header_chars.len() + padding * 2;
|
||||
let pos = self.frame_idx.load(Ordering::Relaxed) % period;
|
||||
|
||||
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
|
||||
.map(|level| level.has_16m)
|
||||
.unwrap_or(false);
|
||||
|
||||
// Width of the bright band (in characters).
|
||||
let band_half_width = 2.0;
|
||||
|
||||
let mut header_spans: Vec<Span<'static>> = Vec::new();
|
||||
for (i, ch) in header_chars.iter().enumerate() {
|
||||
let i_pos = i as isize + padding as isize;
|
||||
let pos = pos as isize;
|
||||
let dist = (i_pos - pos).abs() as f32;
|
||||
|
||||
let t = if dist <= band_half_width {
|
||||
let x = std::f32::consts::PI * (dist / band_half_width);
|
||||
0.5 * (1.0 + x.cos())
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let brightness = 0.4 + 0.6 * t;
|
||||
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
|
||||
let style = if has_true_color {
|
||||
Style::default()
|
||||
.fg(Color::Rgb(level, level, level))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
// Bold makes dark gray and gray look the same, so don't use it
|
||||
// when true color is not supported.
|
||||
Style::default().fg(color_for_level(level))
|
||||
};
|
||||
|
||||
header_spans.push(Span::styled(ch.to_string(), style));
|
||||
}
|
||||
|
||||
header_spans
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ActiveExecCommandView {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicHeightWidgetRef for &ActiveExecCommandView {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let mut spans = self.shimmering_header_spans();
|
||||
spans.push(Span::styled(
|
||||
" ",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled("running...", Style::default().dim()));
|
||||
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
Line::from(vec!["command".yellow(), " running...".dim()]),
|
||||
Line::from(spans),
|
||||
Line::from(format!("$ {}", self.command)),
|
||||
Line::from(""),
|
||||
];
|
||||
@@ -693,10 +794,18 @@ impl DynamicHeightWidgetRef for ActiveExecCommandView {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
impl WidgetRef for ActiveExecCommandView {
|
||||
impl WidgetRef for &ActiveExecCommandView {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut spans = self.shimmering_header_spans();
|
||||
spans.push(Span::styled(
|
||||
" ",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled("running...", Style::default().dim()));
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
Line::from(vec!["command".yellow(), " running...".dim()]),
|
||||
Line::from(spans),
|
||||
Line::from(format!("$ {}", self.command)),
|
||||
Line::from(""),
|
||||
];
|
||||
@@ -706,6 +815,16 @@ impl WidgetRef for ActiveExecCommandView {
|
||||
}
|
||||
}
|
||||
|
||||
fn color_for_level(level: u8) -> Color {
|
||||
if level < 128 {
|
||||
Color::DarkGray
|
||||
} else if level < 192 {
|
||||
Color::Gray
|
||||
} else {
|
||||
Color::White
|
||||
}
|
||||
}
|
||||
|
||||
fn create_diff_summary(changes: HashMap<PathBuf, FileChange>) -> Vec<String> {
|
||||
// Build a concise, human‑readable summary list similar to the
|
||||
// `git status` short format so the user can reason about the
|
||||
|
||||
Reference in New Issue
Block a user