mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
fun
This commit is contained in:
@@ -87,6 +87,14 @@ pub enum Feature {
|
||||
ShellSnapshot,
|
||||
/// Experimental TUI v2 (viewport) implementation.
|
||||
Tui2,
|
||||
/// Experimental TUI animation set 1.
|
||||
Animation1,
|
||||
/// Experimental TUI animation set 2.
|
||||
Animation2,
|
||||
/// Experimental TUI animation set 3.
|
||||
Animation3,
|
||||
/// Experimental TUI animation set 4.
|
||||
Animation4,
|
||||
/// Enforce UTF8 output in Powershell.
|
||||
PowershellUtf8,
|
||||
}
|
||||
@@ -380,4 +388,28 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Animation1,
|
||||
key: "animation1",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Animation2,
|
||||
key: "animation2",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Animation3,
|
||||
key: "animation3",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Animation4,
|
||||
key: "animation4",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Bottom pane: shows the ChatComposer or a BottomPaneView, if one is active.
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::queued_user_messages::QueuedUserMessages;
|
||||
use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter;
|
||||
@@ -78,6 +79,7 @@ pub(crate) struct BottomPane {
|
||||
ctrl_c_quit_hint: bool,
|
||||
esc_backtrack_hint: bool,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
|
||||
/// Inline status indicator shown above the composer while a task is running.
|
||||
status: Option<StatusIndicatorWidget>,
|
||||
@@ -97,6 +99,7 @@ pub(crate) struct BottomPaneParams {
|
||||
pub(crate) placeholder_text: String,
|
||||
pub(crate) disable_paste_burst: bool,
|
||||
pub(crate) animations_enabled: bool,
|
||||
pub(crate) spinner_set: SpinnerSet,
|
||||
pub(crate) skills: Option<Vec<SkillMetadata>>,
|
||||
}
|
||||
|
||||
@@ -110,6 +113,7 @@ impl BottomPane {
|
||||
placeholder_text,
|
||||
disable_paste_burst,
|
||||
animations_enabled,
|
||||
spinner_set,
|
||||
skills,
|
||||
} = params;
|
||||
let mut composer = ChatComposer::new(
|
||||
@@ -134,6 +138,7 @@ impl BottomPane {
|
||||
queued_user_messages: QueuedUserMessages::new(),
|
||||
esc_backtrack_hint: false,
|
||||
animations_enabled,
|
||||
spinner_set,
|
||||
context_window_percent: None,
|
||||
context_window_used_tokens: None,
|
||||
}
|
||||
@@ -353,6 +358,7 @@ impl BottomPane {
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
self.animations_enabled,
|
||||
self.spinner_set,
|
||||
));
|
||||
}
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
@@ -379,6 +385,7 @@ impl BottomPane {
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
self.animations_enabled,
|
||||
self.spinner_set,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -636,6 +643,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
pane.push_approval_request(exec_request(), &features);
|
||||
@@ -659,6 +667,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -693,6 +702,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -760,6 +770,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -772,7 +783,7 @@ mod tests {
|
||||
pane.render(area, &mut buf);
|
||||
|
||||
let bufs = snapshot_buffer(&buf);
|
||||
assert!(bufs.contains("• Working"), "expected Working header");
|
||||
assert!(bufs.contains("Working"), "expected Working header");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -787,6 +798,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -818,6 +830,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -846,6 +859,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
source: tui/src/bottom_pane/mod.rs
|
||||
expression: "render_snapshot(&pane, area)"
|
||||
---
|
||||
• Working (0s • esc to interru
|
||||
o... Working (0s • esc to inte
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
source: tui/src/bottom_pane/mod.rs
|
||||
expression: "render_snapshot(&pane, area)"
|
||||
---
|
||||
• Working (0s • esc to interrupt)
|
||||
o... Working (0s • esc to interrupt)
|
||||
↳ Queued follow-up question
|
||||
⌥ + ↑ edit
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::ApprovalRequest;
|
||||
@@ -340,10 +341,12 @@ pub(crate) struct ChatWidget {
|
||||
reasoning_buffer: String,
|
||||
// Accumulates full reasoning content for transcript-only recording
|
||||
full_reasoning_buffer: String,
|
||||
reasoning_header_emitted: bool,
|
||||
// Current status header shown in the status indicator.
|
||||
current_status_header: String,
|
||||
// Previous status header to restore after a transient stream retry.
|
||||
retry_status_header: Option<String>,
|
||||
spinner_set: SpinnerSet,
|
||||
thread_id: Option<ThreadId>,
|
||||
frame_requester: FrameRequester,
|
||||
// Whether to include the initial welcome banner on session configured
|
||||
@@ -526,13 +529,13 @@ impl ChatWidget {
|
||||
// (between **/**) as the chunk header. Show this header as status.
|
||||
self.reasoning_buffer.push_str(&delta);
|
||||
|
||||
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.
|
||||
if !self.reasoning_header_emitted {
|
||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||
self.add_to_history(history_cell::new_reasoning_header(header));
|
||||
self.reasoning_header_emitted = true;
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_agent_reasoning_final(&mut self) {
|
||||
@@ -545,6 +548,7 @@ impl ChatWidget {
|
||||
}
|
||||
self.reasoning_buffer.clear();
|
||||
self.full_reasoning_buffer.clear();
|
||||
self.reasoning_header_emitted = false;
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -553,6 +557,7 @@ impl ChatWidget {
|
||||
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
|
||||
self.full_reasoning_buffer.push_str("\n\n");
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header_emitted = false;
|
||||
}
|
||||
|
||||
// Raw reasoning uses the same flow as summarized reasoning
|
||||
@@ -565,6 +570,7 @@ impl ChatWidget {
|
||||
self.set_status_header(String::from("Working"));
|
||||
self.full_reasoning_buffer.clear();
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header_emitted = false;
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -1200,6 +1206,7 @@ impl ChatWidget {
|
||||
source,
|
||||
ev.interaction_input.clone(),
|
||||
self.config.animations,
|
||||
self.spinner_set,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -1350,6 +1357,7 @@ impl ChatWidget {
|
||||
ev.source,
|
||||
interaction_input,
|
||||
self.config.animations,
|
||||
self.spinner_set,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -1363,6 +1371,7 @@ impl ChatWidget {
|
||||
ev.call_id,
|
||||
ev.invocation,
|
||||
self.config.animations,
|
||||
self.spinner_set,
|
||||
)));
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -1388,6 +1397,7 @@ impl ChatWidget {
|
||||
call_id,
|
||||
invocation,
|
||||
self.config.animations,
|
||||
self.spinner_set,
|
||||
);
|
||||
let extra_cell = cell.complete(duration, result);
|
||||
self.active_cell = Some(Box::new(cell));
|
||||
@@ -1417,6 +1427,7 @@ impl ChatWidget {
|
||||
} = common;
|
||||
let mut config = config;
|
||||
config.model = Some(model.clone());
|
||||
let spinner_set = SpinnerSet::from_features(&config.features);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager);
|
||||
@@ -1433,6 +1444,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder,
|
||||
disable_paste_burst: config.disable_paste_burst,
|
||||
animations_enabled: config.animations,
|
||||
spinner_set,
|
||||
skills: None,
|
||||
}),
|
||||
active_cell: None,
|
||||
@@ -1461,8 +1473,10 @@ impl ChatWidget {
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
retry_status_header: None,
|
||||
spinner_set,
|
||||
thread_id: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
show_welcome_banner: is_first_run,
|
||||
@@ -1503,6 +1517,7 @@ impl ChatWidget {
|
||||
} = common;
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||
let spinner_set = SpinnerSet::from_features(&config.features);
|
||||
|
||||
let codex_op_tx =
|
||||
spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone());
|
||||
@@ -1519,6 +1534,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder,
|
||||
disable_paste_burst: config.disable_paste_burst,
|
||||
animations_enabled: config.animations,
|
||||
spinner_set,
|
||||
skills: None,
|
||||
}),
|
||||
active_cell: None,
|
||||
@@ -1547,8 +1563,10 @@ impl ChatWidget {
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
retry_status_header: None,
|
||||
spinner_set,
|
||||
thread_id: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
show_welcome_banner: false,
|
||||
|
||||
+28
-1
@@ -2,6 +2,31 @@
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
• I’m going to search the repo for where “Change Approved” is rendered to update
|
||||
that view.
|
||||
|
||||
@@ -9,7 +34,9 @@ expression: term.backend().vt100().screen().contents()
|
||||
└ Search Change Approved
|
||||
Read diff_render.rs
|
||||
|
||||
• Investigating rendering code (0s • esc to interrupt)
|
||||
• Investigating rendering code
|
||||
|
||||
.o.. Working (0s • esc to interrupt)
|
||||
|
||||
|
||||
› Summarize recent commits
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
.o.. Working (0s • esc to interrupt)
|
||||
↳ Hello, world! 0
|
||||
↳ Hello, world! 1
|
||||
↳ Hello, world! 2
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: blob1
|
||||
expression: active_blob(&chat)
|
||||
---
|
||||
• Exploring
|
||||
>... Exploring
|
||||
└ List ls -la
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: blob3
|
||||
expression: active_blob(&chat)
|
||||
---
|
||||
• Exploring
|
||||
<... Exploring
|
||||
└ List ls -la
|
||||
Read foo.txt
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/chatwidget/tests.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"• Booting MCP server: alpha (0s • esc to interrupt) "
|
||||
"o... Booting MCP server: alpha (0s • esc to interrupt) "
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 1577
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"• Analyzing (0s • esc to interrupt) "
|
||||
".o.. Working (0s • esc to interrupt) "
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::test_backend::VT100Backend;
|
||||
@@ -363,6 +364,7 @@ async fn make_chatwidget_manual(
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: cfg.animations,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
skills: None,
|
||||
});
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
|
||||
@@ -394,8 +396,10 @@ async fn make_chatwidget_manual(
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
retry_status_header: None,
|
||||
spinner_set: SpinnerSet::Default,
|
||||
thread_id: None,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
show_welcome_banner: true,
|
||||
|
||||
@@ -9,4 +9,3 @@ pub(crate) use render::OutputLinesParams;
|
||||
pub(crate) use render::TOOL_CALL_MAX_LINES;
|
||||
pub(crate) use render::new_active_exec_command;
|
||||
pub(crate) use render::output_lines;
|
||||
pub(crate) use render::spinner;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use codex_core::protocol::ExecCommandSource;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
|
||||
@@ -29,13 +30,15 @@ pub(crate) struct ExecCall {
|
||||
pub(crate) struct ExecCell {
|
||||
pub(crate) calls: Vec<ExecCall>,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
pub(crate) fn new(call: ExecCall, animations_enabled: bool) -> Self {
|
||||
pub(crate) fn new(call: ExecCall, animations_enabled: bool, spinner_set: SpinnerSet) -> Self {
|
||||
Self {
|
||||
calls: vec![call],
|
||||
animations_enabled,
|
||||
spinner_set,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +64,7 @@ impl ExecCell {
|
||||
Some(Self {
|
||||
calls: [self.calls.clone(), vec![call]].concat(),
|
||||
animations_enabled: self.animations_enabled,
|
||||
spinner_set: self.spinner_set,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -121,6 +125,10 @@ impl ExecCell {
|
||||
self.animations_enabled
|
||||
}
|
||||
|
||||
pub(crate) fn spinner_set(&self) -> SpinnerSet {
|
||||
self.spinner_set
|
||||
}
|
||||
|
||||
pub(crate) fn iter_calls(&self) -> impl Iterator<Item = &ExecCall> {
|
||||
self.calls.iter()
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ use std::time::Instant;
|
||||
use super::model::CommandOutput;
|
||||
use super::model::ExecCall;
|
||||
use super::model::ExecCell;
|
||||
use crate::animations::spinners::SpinnerKind;
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use crate::animations::spinners::spinner;
|
||||
use crate::animations::spinners::spinner_seed;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::render::highlight::highlight_bash_to_lines;
|
||||
use crate::render::line_utils::prefix_lines;
|
||||
use crate::render::line_utils::push_owned_lines;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_line;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
@@ -42,6 +45,7 @@ pub(crate) fn new_active_exec_command(
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<String>,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
) -> ExecCell {
|
||||
ExecCell::new(
|
||||
ExecCall {
|
||||
@@ -55,6 +59,7 @@ pub(crate) fn new_active_exec_command(
|
||||
interaction_input,
|
||||
},
|
||||
animations_enabled,
|
||||
spinner_set,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,20 +182,12 @@ pub(crate) fn output_lines(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spinner(start_time: Option<Instant>, animations_enabled: bool) -> Span<'static> {
|
||||
if !animations_enabled {
|
||||
return "•".dim();
|
||||
}
|
||||
let elapsed = start_time.map(|st| st.elapsed()).unwrap_or_default();
|
||||
if supports_color::on_cached(supports_color::Stream::Stdout)
|
||||
.map(|level| level.has_16m)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
shimmer_spans("•")[0].clone()
|
||||
} else {
|
||||
let blink_on = (elapsed.as_millis() / 600).is_multiple_of(2);
|
||||
if blink_on { "•".into() } else { "◦".dim() }
|
||||
}
|
||||
fn spinner_seed_for_active_call(calls: &[ExecCall]) -> u64 {
|
||||
calls
|
||||
.iter()
|
||||
.find(|call| call.output.is_none())
|
||||
.map(|call| spinner_seed(&call.call_id))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
impl HistoryCell for ExecCell {
|
||||
@@ -254,9 +251,16 @@ impl HistoryCell for ExecCell {
|
||||
impl ExecCell {
|
||||
fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
let spinner_seed = spinner_seed_for_active_call(&self.calls);
|
||||
out.push(Line::from(vec![
|
||||
if self.is_active() {
|
||||
spinner(self.active_start_time(), self.animations_enabled())
|
||||
spinner(
|
||||
self.spinner_set(),
|
||||
SpinnerKind::Exploring,
|
||||
self.active_start_time(),
|
||||
self.animations_enabled(),
|
||||
spinner_seed,
|
||||
)
|
||||
} else {
|
||||
"•".dim()
|
||||
},
|
||||
@@ -360,13 +364,26 @@ impl ExecCell {
|
||||
panic!("Expected exactly one call in a command display cell");
|
||||
};
|
||||
let layout = EXEC_DISPLAY_LAYOUT;
|
||||
let is_interaction = call.is_unified_exec_interaction();
|
||||
let success = call.output.as_ref().map(|o| o.exit_code == 0);
|
||||
let bullet = match success {
|
||||
Some(true) => "•".green().bold(),
|
||||
Some(false) => "•".red().bold(),
|
||||
None => spinner(call.start_time, self.animations_enabled()),
|
||||
None => {
|
||||
let kind = if is_interaction {
|
||||
SpinnerKind::Waiting
|
||||
} else {
|
||||
SpinnerKind::Executing
|
||||
};
|
||||
spinner(
|
||||
self.spinner_set(),
|
||||
kind,
|
||||
call.start_time,
|
||||
self.animations_enabled(),
|
||||
spinner_seed(&call.call_id),
|
||||
)
|
||||
}
|
||||
};
|
||||
let is_interaction = call.is_unified_exec_interaction();
|
||||
let title = if is_interaction {
|
||||
""
|
||||
} else if self.is_active() {
|
||||
@@ -681,7 +698,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
};
|
||||
|
||||
let cell = ExecCell::new(call, false);
|
||||
let cell = ExecCell::new(call, false, SpinnerSet::Default);
|
||||
|
||||
// Use a narrow width so each logical line wraps into many on-screen lines.
|
||||
let lines = cell.command_display_lines(width);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::animations::spinners::SpinnerKind;
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use crate::animations::spinners::spinner;
|
||||
use crate::animations::spinners::spinner_seed;
|
||||
use crate::diff_render::create_diff_summary;
|
||||
use crate::diff_render::display_path_for;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::OutputLinesParams;
|
||||
use crate::exec_cell::TOOL_CALL_MAX_LINES;
|
||||
use crate::exec_cell::output_lines;
|
||||
use crate::exec_cell::spinner;
|
||||
use crate::exec_command::relativize_to_home;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::live_wrap::take_prefix_by_width;
|
||||
@@ -1093,6 +1096,7 @@ pub(crate) struct McpToolCallCell {
|
||||
duration: Option<Duration>,
|
||||
result: Option<Result<mcp_types::CallToolResult, String>>,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
}
|
||||
|
||||
impl McpToolCallCell {
|
||||
@@ -1100,6 +1104,7 @@ impl McpToolCallCell {
|
||||
call_id: String,
|
||||
invocation: McpInvocation,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
) -> Self {
|
||||
Self {
|
||||
call_id,
|
||||
@@ -1108,6 +1113,7 @@ impl McpToolCallCell {
|
||||
duration: None,
|
||||
result: None,
|
||||
animations_enabled,
|
||||
spinner_set,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,7 +1175,13 @@ impl HistoryCell for McpToolCallCell {
|
||||
let bullet = match status {
|
||||
Some(true) => "•".green().bold(),
|
||||
Some(false) => "•".red().bold(),
|
||||
None => spinner(Some(self.start_time), self.animations_enabled),
|
||||
None => spinner(
|
||||
self.spinner_set,
|
||||
SpinnerKind::Tool,
|
||||
Some(self.start_time),
|
||||
self.animations_enabled,
|
||||
spinner_seed(&self.call_id),
|
||||
),
|
||||
};
|
||||
let header_text = if status.is_some() {
|
||||
"Called"
|
||||
@@ -1258,8 +1270,9 @@ pub(crate) fn new_active_mcp_tool_call(
|
||||
call_id: String,
|
||||
invocation: McpInvocation,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
) -> McpToolCallCell {
|
||||
McpToolCallCell::new(call_id, invocation, animations_enabled)
|
||||
McpToolCallCell::new(call_id, invocation, animations_enabled, spinner_set)
|
||||
}
|
||||
|
||||
pub(crate) fn new_web_search_call(query: String) -> PrefixedWrappedHistoryCell {
|
||||
@@ -1660,6 +1673,11 @@ pub(crate) fn new_view_image_tool_call(path: PathBuf, cwd: &Path) -> PlainHistor
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_reasoning_header(header: String) -> PlainHistoryCell {
|
||||
let lines: Vec<Line<'static>> = vec![vec!["• ".dim(), header.bold()].into()];
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_reasoning_summary_block(full_reasoning_buffer: String) -> Box<dyn HistoryCell> {
|
||||
let full_reasoning_buffer = full_reasoning_buffer.trim();
|
||||
if let Some(open) = full_reasoning_buffer.find("**") {
|
||||
@@ -2022,7 +2040,7 @@ mod tests {
|
||||
})),
|
||||
};
|
||||
|
||||
let cell = new_active_mcp_tool_call("call-1".into(), invocation, true);
|
||||
let cell = new_active_mcp_tool_call("call-1".into(), invocation, true, SpinnerSet::Default);
|
||||
let rendered = render_lines(&cell.display_lines(80)).join("\n");
|
||||
|
||||
insta::assert_snapshot!(rendered);
|
||||
@@ -2049,7 +2067,8 @@ mod tests {
|
||||
structured_content: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-2".into(), invocation, true);
|
||||
let mut cell =
|
||||
new_active_mcp_tool_call("call-2".into(), invocation, true, SpinnerSet::Default);
|
||||
assert!(
|
||||
cell.complete(Duration::from_millis(1420), Ok(result))
|
||||
.is_none()
|
||||
@@ -2071,7 +2090,8 @@ mod tests {
|
||||
})),
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-3".into(), invocation, true);
|
||||
let mut cell =
|
||||
new_active_mcp_tool_call("call-3".into(), invocation, true, SpinnerSet::Default);
|
||||
assert!(
|
||||
cell.complete(Duration::from_secs(2), Err("network timeout".into()))
|
||||
.is_none()
|
||||
@@ -2115,7 +2135,8 @@ mod tests {
|
||||
structured_content: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-4".into(), invocation, true);
|
||||
let mut cell =
|
||||
new_active_mcp_tool_call("call-4".into(), invocation, true, SpinnerSet::Default);
|
||||
assert!(
|
||||
cell.complete(Duration::from_millis(640), Ok(result))
|
||||
.is_none()
|
||||
@@ -2147,7 +2168,8 @@ mod tests {
|
||||
structured_content: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-5".into(), invocation, true);
|
||||
let mut cell =
|
||||
new_active_mcp_tool_call("call-5".into(), invocation, true, SpinnerSet::Default);
|
||||
assert!(
|
||||
cell.complete(Duration::from_millis(1280), Ok(result))
|
||||
.is_none()
|
||||
@@ -2186,7 +2208,8 @@ mod tests {
|
||||
structured_content: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-6".into(), invocation, true);
|
||||
let mut cell =
|
||||
new_active_mcp_tool_call("call-6".into(), invocation, true, SpinnerSet::Default);
|
||||
assert!(
|
||||
cell.complete(Duration::from_millis(320), Ok(result))
|
||||
.is_none()
|
||||
@@ -2272,6 +2295,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
// Mark call complete so markers are ✓
|
||||
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
|
||||
@@ -2299,6 +2323,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
// Call 1: Search only
|
||||
cell.complete_call("c1", CommandOutput::default(), Duration::from_millis(1));
|
||||
@@ -2368,6 +2393,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
cell.complete_call("c1", CommandOutput::default(), Duration::from_millis(1));
|
||||
let lines = cell.display_lines(80);
|
||||
@@ -2392,6 +2418,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
// Mark call complete so it renders as "Ran"
|
||||
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
|
||||
@@ -2418,6 +2445,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
|
||||
// Wide enough that it fits inline
|
||||
@@ -2442,6 +2470,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
|
||||
let lines = cell.display_lines(24);
|
||||
@@ -2465,6 +2494,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
|
||||
let lines = cell.display_lines(80);
|
||||
@@ -2489,6 +2519,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
|
||||
let lines = cell.display_lines(28);
|
||||
@@ -2513,6 +2544,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
let stderr: String = (1..=10)
|
||||
.map(|n| n.to_string())
|
||||
@@ -2563,6 +2595,7 @@ mod tests {
|
||||
interaction_input: None,
|
||||
},
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
|
||||
let stderr = "error: first line on stderr\nerror: second line on stderr".to_string();
|
||||
|
||||
@@ -32,6 +32,7 @@ use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
mod additional_dirs;
|
||||
mod animations;
|
||||
mod app;
|
||||
mod app_backtrack;
|
||||
mod app_event;
|
||||
|
||||
@@ -753,6 +753,7 @@ mod tests {
|
||||
ExecCommandSource::Agent,
|
||||
None,
|
||||
true,
|
||||
crate::animations::spinners::SpinnerSet::Default,
|
||||
);
|
||||
exec_cell.complete_call(
|
||||
"exec-1",
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
assertion_line: 1740
|
||||
expression: rendered
|
||||
---
|
||||
• Calling search.find_docs({"query":"ratatui styling","limit":3})
|
||||
[##] Calling search.find_docs({"query":"ratatui styling","limit":3})
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
source: tui/src/status_indicator_widget.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"• Working (0s • esc "
|
||||
"o... Working (0s • e"
|
||||
" "
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
source: tui/src/status_indicator_widget.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"• Working (0s • esc to interrupt) "
|
||||
"o... Working (0s • esc to interrupt) "
|
||||
" "
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
source: tui/src/status_indicator_widget.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"• Working (0s) "
|
||||
".... Working (0s) "
|
||||
" └ A man a plan a canal "
|
||||
" panama "
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use codex_core::protocol::Op;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -16,9 +18,14 @@ use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::animations::spinners::SpinnerKind;
|
||||
use crate::animations::spinners::SpinnerSet;
|
||||
use crate::animations::spinners::animation3_spans;
|
||||
use crate::animations::spinners::animation4_spans;
|
||||
use crate::animations::spinners::spinner;
|
||||
use crate::animations::spinners::spinner_seed;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::exec_cell::spinner;
|
||||
use crate::key_hint;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
@@ -42,6 +49,8 @@ pub(crate) struct StatusIndicatorWidget {
|
||||
app_event_tx: AppEventSender,
|
||||
frame_requester: FrameRequester,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
spinner_seed_offset: u64,
|
||||
}
|
||||
|
||||
// Format elapsed seconds into a compact human-friendly form used by the status line.
|
||||
@@ -66,7 +75,12 @@ impl StatusIndicatorWidget {
|
||||
app_event_tx: AppEventSender,
|
||||
frame_requester: FrameRequester,
|
||||
animations_enabled: bool,
|
||||
spinner_set: SpinnerSet,
|
||||
) -> Self {
|
||||
let seed_offset = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
details: None,
|
||||
@@ -74,10 +88,11 @@ impl StatusIndicatorWidget {
|
||||
elapsed_running: Duration::ZERO,
|
||||
last_resume_at: Instant::now(),
|
||||
is_paused: false,
|
||||
|
||||
app_event_tx,
|
||||
frame_requester,
|
||||
animations_enabled,
|
||||
spinner_set,
|
||||
spinner_seed_offset: seed_offset,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +103,7 @@ impl StatusIndicatorWidget {
|
||||
/// Update the animated header label (left of the brackets).
|
||||
pub(crate) fn update_header(&mut self, header: String) {
|
||||
self.header = header;
|
||||
self.spinner_seed_offset = self.spinner_seed_offset.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// Update the details text shown below the header.
|
||||
@@ -207,32 +223,88 @@ impl Renderable for StatusIndicatorWidget {
|
||||
let elapsed_duration = self.elapsed_duration_at(now);
|
||||
let pretty_elapsed = fmt_elapsed_compact(elapsed_duration.as_secs());
|
||||
|
||||
let mut spans = Vec::with_capacity(5);
|
||||
spans.push(spinner(Some(self.last_resume_at), self.animations_enabled));
|
||||
spans.push(" ".into());
|
||||
if self.animations_enabled {
|
||||
spans.extend(shimmer_spans(&self.header));
|
||||
} else if !self.header.is_empty() {
|
||||
spans.push(self.header.clone().into());
|
||||
}
|
||||
spans.push(" ".into());
|
||||
if self.show_interrupt_hint {
|
||||
spans.extend(vec![
|
||||
format!("({pretty_elapsed} • ").dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to interrupt)".dim(),
|
||||
]);
|
||||
let spinner_kind = SpinnerKind::from_header(&self.header);
|
||||
let spinner_seed = if self.header.is_empty() {
|
||||
spinner_seed("working")
|
||||
} else {
|
||||
spans.push(format!("({pretty_elapsed})").dim());
|
||||
}
|
||||
|
||||
spinner_seed(&self.header)
|
||||
};
|
||||
let mut lines = Vec::new();
|
||||
lines.push(Line::from(spans));
|
||||
if area.height > 1 {
|
||||
// If there is enough space, add the details lines below the header.
|
||||
if matches!(
|
||||
self.spinner_set,
|
||||
SpinnerSet::Animation3 | SpinnerSet::Animation4
|
||||
) {
|
||||
let frame = match self.spinner_set {
|
||||
SpinnerSet::Animation3 => animation3_spans(
|
||||
spinner_kind,
|
||||
Some(self.last_resume_at),
|
||||
self.animations_enabled,
|
||||
spinner_seed ^ self.spinner_seed_offset,
|
||||
),
|
||||
SpinnerSet::Animation4 => animation4_spans(
|
||||
spinner_kind,
|
||||
Some(self.last_resume_at),
|
||||
self.animations_enabled,
|
||||
spinner_seed ^ self.spinner_seed_offset,
|
||||
),
|
||||
_ => unreachable!("animation set mismatch"),
|
||||
};
|
||||
let mut spans = Vec::with_capacity(10);
|
||||
spans.push(frame.face);
|
||||
spans.push(" ".into());
|
||||
if self.spinner_set == SpinnerSet::Animation4 && self.animations_enabled {
|
||||
spans.extend(shimmer_spans(frame.text.content.as_ref()));
|
||||
} else {
|
||||
spans.push(frame.text);
|
||||
}
|
||||
spans.push(" ".into());
|
||||
if self.show_interrupt_hint {
|
||||
spans.extend(vec![
|
||||
format!("({pretty_elapsed} • ").dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to interrupt)".dim(),
|
||||
]);
|
||||
} else {
|
||||
spans.push(format!("({pretty_elapsed})").dim());
|
||||
}
|
||||
lines.push(Line::from(spans));
|
||||
|
||||
let details = self.wrapped_details_lines(area.width);
|
||||
let max_details = usize::from(area.height.saturating_sub(1));
|
||||
lines.extend(details.into_iter().take(max_details));
|
||||
} else {
|
||||
let mut spans = Vec::with_capacity(5);
|
||||
spans.push(spinner(
|
||||
self.spinner_set,
|
||||
spinner_kind,
|
||||
Some(self.last_resume_at),
|
||||
self.animations_enabled,
|
||||
spinner_seed ^ self.spinner_seed_offset,
|
||||
));
|
||||
spans.push(" ".into());
|
||||
if self.animations_enabled {
|
||||
spans.extend(shimmer_spans(&self.header));
|
||||
} else if !self.header.is_empty() {
|
||||
spans.push(self.header.clone().into());
|
||||
}
|
||||
spans.push(" ".into());
|
||||
if self.show_interrupt_hint {
|
||||
spans.extend(vec![
|
||||
format!("({pretty_elapsed} • ").dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to interrupt)".dim(),
|
||||
]);
|
||||
} else {
|
||||
spans.push(format!("({pretty_elapsed})").dim());
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
if area.height > 1 {
|
||||
// If there is enough space, add the details lines below the header.
|
||||
let details = self.wrapped_details_lines(area.width);
|
||||
let max_details = usize::from(area.height.saturating_sub(1));
|
||||
lines.extend(details.into_iter().take(max_details));
|
||||
}
|
||||
}
|
||||
|
||||
Paragraph::new(Text::from(lines)).render_ref(area, buf);
|
||||
@@ -270,7 +342,12 @@ mod tests {
|
||||
fn renders_with_working_header() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let w = StatusIndicatorWidget::new(
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
|
||||
// Render into a fixed-size test terminal and snapshot the backend.
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 2)).expect("terminal");
|
||||
@@ -284,7 +361,12 @@ mod tests {
|
||||
fn renders_truncated() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let w = StatusIndicatorWidget::new(
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
|
||||
// Render into a fixed-size test terminal and snapshot the backend.
|
||||
let mut terminal = Terminal::new(TestBackend::new(20, 2)).expect("terminal");
|
||||
@@ -298,7 +380,12 @@ mod tests {
|
||||
fn renders_wrapped_details_panama_two_lines() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), false);
|
||||
let mut w = StatusIndicatorWidget::new(
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
false,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
w.update_details(Some("A man a plan a canal panama".to_string()));
|
||||
w.set_interrupt_hint_visible(false);
|
||||
|
||||
@@ -319,8 +406,12 @@ mod tests {
|
||||
fn timer_pauses_when_requested() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut widget =
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let mut widget = StatusIndicatorWidget::new(
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
|
||||
let baseline = Instant::now();
|
||||
widget.last_resume_at = baseline;
|
||||
@@ -341,7 +432,12 @@ mod tests {
|
||||
fn details_overflow_adds_ellipsis() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let mut w = StatusIndicatorWidget::new(
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
true,
|
||||
SpinnerSet::Default,
|
||||
);
|
||||
w.update_details(Some("abcd abcd abcd abcd".to_string()));
|
||||
|
||||
let lines = w.wrapped_details_lines(6);
|
||||
|
||||
Reference in New Issue
Block a user