mirror of
https://github.com/openai/codex.git
synced 2026-02-04 07:53:43 +00:00
Compare commits
4 Commits
codex-work
...
fun/animat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
381907f117 | ||
|
|
f63cd0bc83 | ||
|
|
70f09db901 | ||
|
|
eded1864b6 |
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -701,33 +701,6 @@ pub async fn start_mock_server() -> MockServer {
|
||||
server
|
||||
}
|
||||
|
||||
// todo(aibrahim): remove this and use our search matching patterns directly
|
||||
/// Get all POST requests to `/responses` endpoints from the mock server.
|
||||
/// Filters out GET requests (e.g., `/models`) .
|
||||
pub async fn get_responses_requests(server: &MockServer) -> Vec<wiremock::Request> {
|
||||
server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| req.method == "POST" && req.url.path().ends_with("/responses"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// todo(aibrahim): remove this and use our search matching patterns directly
|
||||
/// Get request bodies as JSON values from POST requests to `/responses` endpoints.
|
||||
/// Filters out GET requests (e.g., `/models`) .
|
||||
pub async fn get_responses_request_bodies(server: &MockServer) -> Vec<Value> {
|
||||
get_responses_requests(server)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FunctionCallResponseMocks {
|
||||
pub function_call: ResponseMock,
|
||||
|
||||
@@ -23,10 +23,12 @@ use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
use crate::load_default_config_for_test;
|
||||
use crate::responses::get_responses_request_bodies;
|
||||
use crate::responses::start_mock_server;
|
||||
use crate::streaming_sse::StreamingSseServer;
|
||||
use crate::wait_for_event;
|
||||
use wiremock::Match;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
type ConfigMutator = dyn FnOnce(&mut Config) + Send;
|
||||
type PreBuildHook = dyn FnOnce(&Path) + Send + 'static;
|
||||
@@ -322,7 +324,18 @@ impl TestCodexHarness {
|
||||
}
|
||||
|
||||
pub async fn request_bodies(&self) -> Vec<Value> {
|
||||
get_responses_request_bodies(&self.server).await
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
self.server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn function_call_output_value(&self, call_id: &str) -> Value {
|
||||
|
||||
@@ -31,7 +31,6 @@ use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::responses::ev_completed_with_tokens;
|
||||
use core_test_support::responses::get_responses_requests;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::mount_sse_once_match;
|
||||
use core_test_support::responses::sse;
|
||||
@@ -47,6 +46,7 @@ use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
use wiremock::Match;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
@@ -54,6 +54,7 @@ use wiremock::matchers::body_string_contains;
|
||||
use wiremock::matchers::header_regex;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
use wiremock::matchers::path_regex;
|
||||
use wiremock::matchers::query_param;
|
||||
|
||||
/// Build minimal SSE stream with completed marker using the JSON fixture.
|
||||
@@ -374,7 +375,14 @@ async fn includes_conversation_id_and_model_headers_in_request() {
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
// get request from the server
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
let request = requests
|
||||
.first()
|
||||
.expect("expected POST request to /responses");
|
||||
@@ -500,7 +508,14 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
// get request from the server
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
let request = requests
|
||||
.first()
|
||||
.expect("expected POST request to /responses");
|
||||
@@ -1233,7 +1248,14 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
}
|
||||
}
|
||||
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(requests.len(), 1, "expected a single POST request");
|
||||
let body: serde_json::Value = requests[0]
|
||||
.body_json()
|
||||
@@ -1847,7 +1869,14 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
// Inspect the three captured requests.
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(requests.len(), 3, "expected 3 requests (one per turn)");
|
||||
|
||||
// Replace full-array compare with tail-only raw JSON compare using a single hard-coded value.
|
||||
|
||||
@@ -31,7 +31,6 @@ use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_completed_with_tokens;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::get_responses_requests;
|
||||
use core_test_support::responses::mount_compact_json_once;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::mount_sse_once_match;
|
||||
@@ -41,7 +40,9 @@ use core_test_support::responses::sse_failed;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use wiremock::Match;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::matchers::path_regex;
|
||||
// --- Test helpers -----------------------------------------------------------
|
||||
|
||||
pub(super) const FIRST_REPLY: &str = "FIRST_REPLY";
|
||||
@@ -358,7 +359,14 @@ async fn manual_compact_uses_custom_prompt() {
|
||||
assert_eq!(message, COMPACT_WARNING_MESSAGE);
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
let body = requests
|
||||
.iter()
|
||||
.find_map(|req| req.body_json::<serde_json::Value>().ok())
|
||||
@@ -586,7 +594,14 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
// collect the requests payloads from the model
|
||||
let requests_payloads = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests_payloads = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let body = requests_payloads[0]
|
||||
.body_json::<serde_json::Value>()
|
||||
@@ -1111,7 +1126,14 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
4,
|
||||
@@ -1897,7 +1919,14 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
|
||||
"auto compact should not emit task lifecycle events"
|
||||
);
|
||||
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
let request_bodies: Vec<String> = requests
|
||||
.into_iter()
|
||||
.map(|request| String::from_utf8(request.body).unwrap_or_default())
|
||||
|
||||
@@ -26,7 +26,6 @@ use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::get_responses_request_bodies;
|
||||
use core_test_support::responses::mount_sse_once_match;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::wait_for_event;
|
||||
@@ -35,7 +34,10 @@ use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Match;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
const AFTER_SECOND_RESUME: &str = "AFTER_SECOND_RESUME";
|
||||
|
||||
@@ -772,7 +774,18 @@ fn normalize_line_endings(value: &mut Value) {
|
||||
}
|
||||
|
||||
async fn gather_request_bodies(server: &MockServer) -> Vec<Value> {
|
||||
let mut bodies = get_responses_request_bodies(server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let mut bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for body in &mut bodies {
|
||||
normalize_line_endings(body);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ use codex_core::review_format::render_review_output_text;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::load_sse_fixture_with_id_from_str;
|
||||
use core_test_support::responses::get_responses_requests;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -32,11 +31,12 @@ use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
use uuid::Uuid;
|
||||
use wiremock::Match;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
/// Verify that submitting `Op::Review` spawns a child task and emits
|
||||
/// EnteredReviewMode -> ExitedReviewMode(None) -> TaskComplete
|
||||
@@ -426,7 +426,14 @@ async fn review_uses_custom_review_model_from_config() {
|
||||
let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
// Assert the request body model equals the configured review model
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
let request = requests
|
||||
.first()
|
||||
.expect("expected POST request to /responses");
|
||||
@@ -547,7 +554,14 @@ async fn review_input_isolated_from_parent_history() {
|
||||
let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
// Assert the request `input` contains the environment context followed by the user review prompt.
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
let request = requests
|
||||
.first()
|
||||
.expect("expected POST request to /responses");
|
||||
@@ -674,7 +688,14 @@ async fn review_history_surfaces_in_parent_session() {
|
||||
// Inspect the second request (parent turn) input contents.
|
||||
// Parent turns include session initial messages (user_instructions, environment_context).
|
||||
// Critically, no messages from the review thread should appear.
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let body = requests[1].body_json::<serde_json::Value>().unwrap();
|
||||
let input = body["input"].as_array().expect("input array");
|
||||
@@ -792,7 +813,14 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
|
||||
let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await;
|
||||
let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let requests = get_responses_requests(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let body = requests[0].body_json::<serde_json::Value>().unwrap();
|
||||
let input = body["input"].as_array().expect("input array");
|
||||
|
||||
@@ -20,7 +20,6 @@ use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::get_responses_request_bodies;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
@@ -38,6 +37,9 @@ use regex_lite::Regex;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tokio::time::Duration;
|
||||
use wiremock::Match;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
fn extract_output_text(item: &Value) -> Option<&str> {
|
||||
item.get("output").and_then(|value| match value {
|
||||
@@ -1241,7 +1243,18 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
let metadata = outputs
|
||||
@@ -1345,7 +1358,18 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
let output = outputs
|
||||
@@ -1470,7 +1494,18 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
|
||||
@@ -1824,7 +1859,18 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
|
||||
@@ -1958,7 +2004,18 @@ PY
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
|
||||
@@ -2067,7 +2124,18 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
|
||||
@@ -2153,7 +2221,18 @@ PY
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
let large_output = outputs.get(call_id).expect("missing large output summary");
|
||||
@@ -2230,7 +2309,18 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
let output = outputs.get(call_id).expect("missing output");
|
||||
@@ -2328,7 +2418,18 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
let startup_output = outputs
|
||||
@@ -2418,7 +2519,18 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert!(!requests.is_empty(), "expected at least one POST request");
|
||||
|
||||
let bodies = get_responses_request_bodies(&server).await;
|
||||
let path_matcher = path_regex(".*/responses$");
|
||||
let bodies = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should not fail")
|
||||
.into_iter()
|
||||
.filter(|req| path_matcher.matches(req))
|
||||
.map(|req| {
|
||||
req.body_json::<Value>()
|
||||
.expect("request body to be valid JSON")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let outputs = collect_tool_outputs(&bodies)?;
|
||||
let output = outputs.get(call_id).expect("missing output");
|
||||
|
||||
1
codex-rs/tui/src/animations/mod.rs
Normal file
1
codex-rs/tui/src/animations/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod spinners;
|
||||
200
codex-rs/tui/src/animations/spinners/mod.rs
Normal file
200
codex-rs/tui/src/animations/spinners/mod.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::features::Features;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Span;
|
||||
|
||||
mod sets;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum SpinnerKind {
|
||||
Thinking,
|
||||
Exploring,
|
||||
Executing,
|
||||
Waiting,
|
||||
Tool,
|
||||
}
|
||||
|
||||
impl SpinnerKind {
|
||||
pub(crate) fn from_header(header: &str) -> Self {
|
||||
let header = header.to_ascii_lowercase();
|
||||
if header.contains("explor") {
|
||||
Self::Exploring
|
||||
} else if header.contains("wait") {
|
||||
Self::Waiting
|
||||
} else if header.contains("run") || header.contains("execut") {
|
||||
Self::Executing
|
||||
} else {
|
||||
Self::Thinking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SpinnerSet {
|
||||
Default,
|
||||
Animation1,
|
||||
Animation2,
|
||||
Animation3,
|
||||
Animation4,
|
||||
}
|
||||
|
||||
impl SpinnerSet {
|
||||
pub(crate) fn from_features(features: &Features) -> Self {
|
||||
if features.enabled(Feature::Animation4) {
|
||||
Self::Animation4
|
||||
} else if features.enabled(Feature::Animation3) {
|
||||
Self::Animation3
|
||||
} else if features.enabled(Feature::Animation2) {
|
||||
Self::Animation2
|
||||
} else if features.enabled(Feature::Animation1) {
|
||||
Self::Animation1
|
||||
} else {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SpinnerTheme {
|
||||
variants: &'static [&'static [&'static str]],
|
||||
tick_ms: u128,
|
||||
idle_frame: &'static str,
|
||||
style: SpinnerStyle,
|
||||
}
|
||||
|
||||
enum SpinnerStyle {
|
||||
Fixed(fn(&'static str) -> Span<'static>),
|
||||
Cycle {
|
||||
colors: &'static [Color],
|
||||
tick_ms: u128,
|
||||
dim_every: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl SpinnerStyle {
|
||||
fn render(&self, frame: &'static str, elapsed_ms: u128, seed: u64) -> Span<'static> {
|
||||
match self {
|
||||
SpinnerStyle::Fixed(style) => style(frame),
|
||||
SpinnerStyle::Cycle {
|
||||
colors,
|
||||
tick_ms,
|
||||
dim_every,
|
||||
} => {
|
||||
if colors.is_empty() {
|
||||
return Span::from(frame).bold();
|
||||
}
|
||||
let step = elapsed_ms / (*tick_ms).max(1);
|
||||
let idx = ((step as u64).wrapping_add(seed) as usize) % colors.len();
|
||||
let mut span = Span::from(frame).fg(colors[idx]).bold();
|
||||
if *dim_every > 0 && (idx as u8) % dim_every == 0 {
|
||||
span = span.dim();
|
||||
}
|
||||
span
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spinner_seed(value: &str) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
value.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub(crate) fn spinner(
|
||||
set: SpinnerSet,
|
||||
kind: SpinnerKind,
|
||||
start_time: Option<Instant>,
|
||||
animations_enabled: bool,
|
||||
seed: u64,
|
||||
) -> Span<'static> {
|
||||
let theme = sets::theme(set, kind);
|
||||
if !animations_enabled {
|
||||
return theme.idle_frame.dim();
|
||||
}
|
||||
|
||||
let elapsed_ms = start_time.map(|st| st.elapsed().as_millis()).unwrap_or(0);
|
||||
let variants = theme.variants;
|
||||
if variants.is_empty() {
|
||||
return theme.idle_frame.dim();
|
||||
}
|
||||
let variant_idx = ((seed ^ kind_seed(kind)) as usize) % variants.len();
|
||||
let frames = variants[variant_idx];
|
||||
if frames.is_empty() {
|
||||
return theme.idle_frame.dim();
|
||||
}
|
||||
|
||||
let tick_ms = theme.tick_ms.max(1);
|
||||
let frame_idx = ((elapsed_ms / tick_ms) % frames.len() as u128) as usize;
|
||||
theme
|
||||
.style
|
||||
.render(frames[frame_idx], elapsed_ms, seed ^ kind_seed(kind))
|
||||
}
|
||||
|
||||
pub(crate) struct Animation3Spans {
|
||||
pub(crate) text: Span<'static>,
|
||||
pub(crate) face: Span<'static>,
|
||||
}
|
||||
|
||||
pub(crate) fn animation3_spans(
|
||||
kind: SpinnerKind,
|
||||
start_time: Option<Instant>,
|
||||
animations_enabled: bool,
|
||||
seed: u64,
|
||||
) -> Animation3Spans {
|
||||
let elapsed_ms = start_time.map(|st| st.elapsed().as_millis()).unwrap_or(0);
|
||||
let frame = sets::animation3::frame(kind, elapsed_ms, animations_enabled, seed);
|
||||
let style = sets::animation3::style_for_kind(kind);
|
||||
let text = style.render(frame.text, elapsed_ms, seed);
|
||||
let face = style.render(frame.face, elapsed_ms, seed);
|
||||
Animation3Spans { text, face }
|
||||
}
|
||||
|
||||
pub(crate) fn animation4_spans(
|
||||
kind: SpinnerKind,
|
||||
start_time: Option<Instant>,
|
||||
animations_enabled: bool,
|
||||
seed: u64,
|
||||
) -> Animation3Spans {
|
||||
let elapsed_ms = start_time.map(|st| st.elapsed().as_millis()).unwrap_or(0);
|
||||
let frame = sets::animation4::frame(kind, elapsed_ms, animations_enabled, seed);
|
||||
let style = sets::animation4::style_for_kind(kind);
|
||||
let text = style.render(frame.text, elapsed_ms, seed);
|
||||
let face = style.render(frame.face, elapsed_ms, seed);
|
||||
Animation3Spans { text, face }
|
||||
}
|
||||
|
||||
fn kind_seed(kind: SpinnerKind) -> u64 {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => 1,
|
||||
SpinnerKind::Exploring => 2,
|
||||
SpinnerKind::Executing => 3,
|
||||
SpinnerKind::Waiting => 4,
|
||||
SpinnerKind::Tool => 5,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn style_thinking(frame: &'static str) -> Span<'static> {
|
||||
frame.magenta().bold()
|
||||
}
|
||||
|
||||
pub(super) fn style_exploring(frame: &'static str) -> Span<'static> {
|
||||
frame.cyan().bold()
|
||||
}
|
||||
|
||||
pub(super) fn style_executing(frame: &'static str) -> Span<'static> {
|
||||
frame.green().bold()
|
||||
}
|
||||
|
||||
pub(super) fn style_waiting(frame: &'static str) -> Span<'static> {
|
||||
frame.yellow().bold()
|
||||
}
|
||||
|
||||
pub(super) fn style_tool(frame: &'static str) -> Span<'static> {
|
||||
frame.blue().bold()
|
||||
}
|
||||
190
codex-rs/tui/src/animations/spinners/sets/animation1.rs
Normal file
190
codex-rs/tui/src/animations/spinners/sets/animation1.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use super::SpinnerKind;
|
||||
use super::SpinnerTheme;
|
||||
use crate::animations::spinners::SpinnerStyle;
|
||||
use crate::animations::spinners::style_executing;
|
||||
use crate::animations::spinners::style_exploring;
|
||||
use crate::animations::spinners::style_thinking;
|
||||
use crate::animations::spinners::style_tool;
|
||||
use crate::animations::spinners::style_waiting;
|
||||
|
||||
pub(super) fn theme(kind: SpinnerKind) -> SpinnerTheme {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => SpinnerTheme {
|
||||
variants: THINKING_VARIANTS,
|
||||
tick_ms: 140,
|
||||
idle_frame: "⠿⠿⠿⠿",
|
||||
style: SpinnerStyle::Fixed(style_thinking),
|
||||
},
|
||||
SpinnerKind::Exploring => SpinnerTheme {
|
||||
variants: EXPLORING_VARIANTS,
|
||||
tick_ms: 90,
|
||||
idle_frame: "▒▒▒▒",
|
||||
style: SpinnerStyle::Fixed(style_exploring),
|
||||
},
|
||||
SpinnerKind::Executing => SpinnerTheme {
|
||||
variants: EXECUTING_VARIANTS,
|
||||
tick_ms: 70,
|
||||
idle_frame: "████",
|
||||
style: SpinnerStyle::Fixed(style_executing),
|
||||
},
|
||||
SpinnerKind::Waiting => SpinnerTheme {
|
||||
variants: WAITING_VARIANTS,
|
||||
tick_ms: 180,
|
||||
idle_frame: "░░░░",
|
||||
style: SpinnerStyle::Fixed(style_waiting),
|
||||
},
|
||||
SpinnerKind::Tool => SpinnerTheme {
|
||||
variants: TOOL_VARIANTS,
|
||||
tick_ms: 120,
|
||||
idle_frame: "▣▣▣▣",
|
||||
style: SpinnerStyle::Fixed(style_tool),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const THINKING_VARIANTS: &[&[&str]] = &[
|
||||
&["⠁⠂⠄⠂", "⠂⠄⠂⠁", "⠄⠂⠁⠂", "⠂⠁⠂⠄"],
|
||||
&["⠈⠐⠠⠐", "⠐⠠⠐⠈", "⠠⠐⠈⠐", "⠐⠈⠐⠠"],
|
||||
&["⠋⠙⠹⠸", "⠙⠹⠸⠋", "⠹⠸⠋⠙", "⠸⠋⠙⠹"],
|
||||
&["⠂⠒⠐⠒", "⠒⠐⠒⠂", "⠐⠒⠂⠒", "⠒⠂⠒⠐"],
|
||||
&["⠉⠒⠤⠒", "⠒⠤⠒⠉", "⠤⠒⠉⠒", "⠒⠉⠒⠤"],
|
||||
&["⠇⠋⠙⠸", "⠋⠙⠸⠇", "⠙⠸⠇⠋", "⠸⠇⠋⠙"],
|
||||
&["⠛⠻⠽⠿", "⠻⠽⠿⠛", "⠽⠿⠛⠻", "⠿⠛⠻⠽"],
|
||||
&["⠠⠔⠂⠔", "⠔⠂⠔⠠", "⠂⠔⠠⠔", "⠔⠠⠔⠂"],
|
||||
&["⠒⠆⠰⠆", "⠆⠰⠆⠒", "⠰⠆⠒⠆", "⠆⠒⠆⠰"],
|
||||
&["⠅⠇⠋⠙", "⠇⠋⠙⠅", "⠋⠙⠅⠇", "⠙⠅⠇⠋"],
|
||||
&["⠟⠯⠷⠿", "⠯⠷⠿⠟", "⠷⠿⠟⠯", "⠿⠟⠯⠷"],
|
||||
&["⠄⠆⠇⠆", "⠆⠇⠆⠄", "⠇⠆⠄⠆", "⠆⠄⠆⠇"],
|
||||
&["◔◑◕◐", "◑◕◐◔", "◕◐◔◑", "◐◔◑◕"],
|
||||
&["◌◍●◍", "◍●◍◌", "●◍◌◍", "◍◌◍●"],
|
||||
&["˙·•·", "·•·˙", "•·˙·", "·˙·•"],
|
||||
&["∞···", "·∞··", "··∞·", "···∞"],
|
||||
&["(-) ", "(-)", " (-", " -"],
|
||||
&["(o )", " (o)", "( o", " o "],
|
||||
&["(o)", "(O)", "(o)", "(O)"],
|
||||
&["(°)", "(o)", "(°)", "(o)"],
|
||||
&["⊙⊙⊙⊙", "⊙⊙◌⊙", "⊙◌⊙⊙", "◌⊙⊙⊙"],
|
||||
&["▤▥▦▧", "▥▦▧▤", "▦▧▤▥", "▧▤▥▦"],
|
||||
&["▖▘▝▗", "▘▝▗▖", "▝▗▖▘", "▗▖▘▝"],
|
||||
];
|
||||
|
||||
const EXPLORING_VARIANTS: &[&[&str]] = &[
|
||||
&[
|
||||
"▌···",
|
||||
"·▌··",
|
||||
"··▌·",
|
||||
"···▌",
|
||||
"···▐",
|
||||
"··▐·",
|
||||
"·▐··",
|
||||
"▐···",
|
||||
],
|
||||
&[
|
||||
"▖···",
|
||||
"·▘··",
|
||||
"··▝·",
|
||||
"···▗",
|
||||
"···▖",
|
||||
"··▘·",
|
||||
"·▝··",
|
||||
"▗···",
|
||||
],
|
||||
&[
|
||||
"▚···",
|
||||
"·▞··",
|
||||
"··▚·",
|
||||
"···▞",
|
||||
"···▚",
|
||||
"··▞·",
|
||||
"·▚··",
|
||||
"▞···",
|
||||
],
|
||||
&[
|
||||
"⟐···",
|
||||
"·⟐··",
|
||||
"··⟐·",
|
||||
"···⟐",
|
||||
"···⟡",
|
||||
"··⟡·",
|
||||
"·⟡··",
|
||||
"⟡···",
|
||||
],
|
||||
&[
|
||||
"⌜···",
|
||||
"·⌜··",
|
||||
"··⌜·",
|
||||
"···⌜",
|
||||
"···⌟",
|
||||
"··⌟·",
|
||||
"·⌟··",
|
||||
"⌟···",
|
||||
],
|
||||
&[
|
||||
"↖···",
|
||||
"·↗··",
|
||||
"··↘·",
|
||||
"···↙",
|
||||
"··↙·",
|
||||
"·↘··",
|
||||
"↗···",
|
||||
"·↖··",
|
||||
],
|
||||
&[
|
||||
"⋅···",
|
||||
"·⋅··",
|
||||
"··⋅·",
|
||||
"···⋅",
|
||||
"··⋅·",
|
||||
"·⋅··",
|
||||
"⋅···",
|
||||
"·⋅··",
|
||||
],
|
||||
&[
|
||||
"⌈⌉··",
|
||||
"·⌈⌉·",
|
||||
"··⌈⌉",
|
||||
"·⌉⌈·",
|
||||
"⌉⌈··",
|
||||
"·⌉⌈·",
|
||||
"··⌉⌈",
|
||||
"·⌈⌉·",
|
||||
],
|
||||
];
|
||||
|
||||
const EXECUTING_VARIANTS: &[&[&str]] = &[
|
||||
&[
|
||||
"▁▂▃▄",
|
||||
"▂▃▄▅",
|
||||
"▃▄▅▆",
|
||||
"▄▅▆▇",
|
||||
"▅▆▇█",
|
||||
"▆▇█▇",
|
||||
"▇█▇▆",
|
||||
"█▇▆▅",
|
||||
],
|
||||
&["▗▖▘▝", "▖▘▝▗", "▘▝▗▖", "▝▗▖▘"],
|
||||
&["▉▊▋▌", "▊▋▌▍", "▋▌▍▎", "▌▍▎▏", "▍▎▏▎", "▎▏▎▍"],
|
||||
&["╶╴╶╴", "╴╶╴╶", "╶╴╶╴", "╴╶╴╶"],
|
||||
&["▶▶ ", " ▶▶ ", " ▶▶", " ▶▶ ", "▶▶ "],
|
||||
&[">>>>", ">>>·", ">>··", ">···", ">>··", ">>>·"],
|
||||
&["▌▌▌▌", "▌▌▌ ", "▌▌ ", "▌ ", "▌▌ ", "▌▌▌ "],
|
||||
&["▛▙▟█", "▙▟█▛", "▟█▛▙", "█▛▙▟"],
|
||||
];
|
||||
|
||||
const WAITING_VARIANTS: &[&[&str]] = &[
|
||||
&["░░░░", "▒▒▒▒", "▓▓▓▓", "▒▒▒▒"],
|
||||
&["░▒▓▒", "▒▓▒░", "▓▒░▒", "▒░▒▓"],
|
||||
&["… ", " … ", " … ", " …", " … ", " … "],
|
||||
&["⠁⠁⠁⠁", "⠂⠂⠂⠂", "⠄⠄⠄⠄", "⠂⠂⠂⠂"],
|
||||
&["⟡⟡⟡⟡", "⟐⟐⟐⟐", "⟡⟡⟡⟡", "⟐⟐⟐⟐"],
|
||||
&["▱▱▱▱", "▰▰▰▰", "▱▱▱▱", "▰▰▰▰"],
|
||||
&["┈┈┈┈", "┉┉┉┉", "┈┈┈┈", "┉┉┉┉"],
|
||||
];
|
||||
|
||||
const TOOL_VARIANTS: &[&[&str]] = &[
|
||||
&["⚙️···", "·⚙️··", "··⚙️·", "···⚙️", "··⚙️·", "·⚙️··"],
|
||||
&["▣▢▣▢", "▢▣▢▣", "▣▢▣▢", "▢▣▢▣"],
|
||||
&["◰◱◲◳", "◱◲◳◰", "◲◳◰◱", "◳◰◱◲"],
|
||||
&["♦◆♦◆", "◆♦◆♦", "♦◆♦◆", "◆♦◆♦"],
|
||||
&["⌁⌁⌁⌁", "⌁⌁⌁⌁", "⌁⌁⌁⌁", "⌁⌁⌁⌁"],
|
||||
];
|
||||
198
codex-rs/tui/src/animations/spinners/sets/animation2.rs
Normal file
198
codex-rs/tui/src/animations/spinners/sets/animation2.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ratatui::style::Color;
|
||||
|
||||
use super::SpinnerKind;
|
||||
use super::SpinnerTheme;
|
||||
use crate::animations::spinners::SpinnerStyle;
|
||||
|
||||
const SCALE: usize = 2;
|
||||
|
||||
const THINKING_COLORS: &[Color] = &[
|
||||
Color::LightMagenta,
|
||||
Color::Magenta,
|
||||
Color::LightBlue,
|
||||
Color::LightCyan,
|
||||
];
|
||||
const EXPLORING_COLORS: &[Color] = &[
|
||||
Color::LightCyan,
|
||||
Color::Cyan,
|
||||
Color::LightBlue,
|
||||
Color::LightGreen,
|
||||
];
|
||||
const EXECUTING_COLORS: &[Color] = &[
|
||||
Color::LightGreen,
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::LightRed,
|
||||
];
|
||||
const WAITING_COLORS: &[Color] = &[
|
||||
Color::LightBlue,
|
||||
Color::Blue,
|
||||
Color::LightCyan,
|
||||
Color::DarkGray,
|
||||
];
|
||||
const TOOL_COLORS: &[Color] = &[
|
||||
Color::LightYellow,
|
||||
Color::Yellow,
|
||||
Color::LightRed,
|
||||
Color::Red,
|
||||
];
|
||||
|
||||
static THINKING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static EXPLORING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static EXECUTING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static WAITING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static TOOL_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
|
||||
static THINKING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static EXPLORING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static EXECUTING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static WAITING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static TOOL_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
|
||||
pub(super) fn theme(kind: SpinnerKind) -> SpinnerTheme {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => SpinnerTheme {
|
||||
variants: scaled_thinking(),
|
||||
tick_ms: 120,
|
||||
idle_frame: scaled_idle("..", &THINKING_IDLE),
|
||||
style: SpinnerStyle::Cycle {
|
||||
colors: THINKING_COLORS,
|
||||
tick_ms: 260,
|
||||
dim_every: 3,
|
||||
},
|
||||
},
|
||||
SpinnerKind::Exploring => SpinnerTheme {
|
||||
variants: scaled_exploring(),
|
||||
tick_ms: 110,
|
||||
idle_frame: scaled_idle("..", &EXPLORING_IDLE),
|
||||
style: SpinnerStyle::Cycle {
|
||||
colors: EXPLORING_COLORS,
|
||||
tick_ms: 210,
|
||||
dim_every: 4,
|
||||
},
|
||||
},
|
||||
SpinnerKind::Executing => SpinnerTheme {
|
||||
variants: scaled_executing(),
|
||||
tick_ms: 80,
|
||||
idle_frame: scaled_idle("==", &EXECUTING_IDLE),
|
||||
style: SpinnerStyle::Cycle {
|
||||
colors: EXECUTING_COLORS,
|
||||
tick_ms: 140,
|
||||
dim_every: 0,
|
||||
},
|
||||
},
|
||||
SpinnerKind::Waiting => SpinnerTheme {
|
||||
variants: scaled_waiting(),
|
||||
tick_ms: 150,
|
||||
idle_frame: scaled_idle("..", &WAITING_IDLE),
|
||||
style: SpinnerStyle::Cycle {
|
||||
colors: WAITING_COLORS,
|
||||
tick_ms: 300,
|
||||
dim_every: 2,
|
||||
},
|
||||
},
|
||||
SpinnerKind::Tool => SpinnerTheme {
|
||||
variants: scaled_tool(),
|
||||
tick_ms: 110,
|
||||
idle_frame: scaled_idle("[]", &TOOL_IDLE),
|
||||
style: SpinnerStyle::Cycle {
|
||||
colors: TOOL_COLORS,
|
||||
tick_ms: 180,
|
||||
dim_every: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn scaled_thinking() -> &'static [&'static [&'static str]] {
|
||||
THINKING_SCALED.get_or_init(|| scale_variants(THINKING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_exploring() -> &'static [&'static [&'static str]] {
|
||||
EXPLORING_SCALED.get_or_init(|| scale_variants(EXPLORING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_executing() -> &'static [&'static [&'static str]] {
|
||||
EXECUTING_SCALED.get_or_init(|| scale_variants(EXECUTING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_waiting() -> &'static [&'static [&'static str]] {
|
||||
WAITING_SCALED.get_or_init(|| scale_variants(WAITING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_tool() -> &'static [&'static [&'static str]] {
|
||||
TOOL_SCALED.get_or_init(|| scale_variants(TOOL_BASE))
|
||||
}
|
||||
|
||||
fn scaled_idle(base: &str, cache: &OnceLock<&'static str>) -> &'static str {
|
||||
cache.get_or_init(|| Box::leak(scale_frame(base, SCALE).into_boxed_str()))
|
||||
}
|
||||
|
||||
fn scale_variants(base: &'static [&'static [&'static str]]) -> &'static [&'static [&'static str]] {
|
||||
let scaled: Vec<&'static [&'static str]> = base
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
let frames: Vec<&'static str> = variant
|
||||
.iter()
|
||||
.map(|frame| Box::leak(scale_frame(frame, SCALE).into_boxed_str()) as &'static str)
|
||||
.collect();
|
||||
Box::leak(frames.into_boxed_slice()) as &'static [&'static str]
|
||||
})
|
||||
.collect();
|
||||
Box::leak(scaled.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn scale_frame(frame: &str, scale: usize) -> String {
|
||||
if scale <= 1 {
|
||||
return frame.to_string();
|
||||
}
|
||||
let mut scaled = String::with_capacity(frame.len() * scale);
|
||||
for ch in frame.chars() {
|
||||
for _ in 0..scale {
|
||||
scaled.push(ch);
|
||||
}
|
||||
}
|
||||
scaled
|
||||
}
|
||||
|
||||
const THINKING_BASE: &[&[&str]] = &[
|
||||
&["..", "o.", "oo", ".o", "..", ".o", "oo", "o."],
|
||||
&["<>", "><", "<>", "><"],
|
||||
&["()", ")(", "()", ")("],
|
||||
&["??", "!!", "??", "!!"],
|
||||
&["░░", "▒▒", "▓▓", "▒▒"],
|
||||
];
|
||||
|
||||
const EXPLORING_BASE: &[&[&str]] = &[
|
||||
&[">.", ".>", "..", "<.", ".<", ".."],
|
||||
&["^.", ".^", "v.", ".v", "^.", ".^"],
|
||||
&["/\\", "\\/", "/\\", "\\/"],
|
||||
&["[]", "][", "[]", "]["],
|
||||
&["..", "::", ";;", "::"],
|
||||
];
|
||||
|
||||
const EXECUTING_BASE: &[&[&str]] = &[
|
||||
&["==", "=-", "--", "-=", "=="],
|
||||
&["++", "+*", "**", "*+", "++"],
|
||||
&["##", "%%", "@@", "%%"],
|
||||
&["[]", "[=", "[]", "=]"],
|
||||
&["▣▣", "▣▢", "▢▣", "▣▣"],
|
||||
];
|
||||
|
||||
const WAITING_BASE: &[&[&str]] = &[
|
||||
&["..", ". ", " ", " .", ".."],
|
||||
&["--", " -", " ", "- ", "--"],
|
||||
&["..", "o.", "oo", ".o"],
|
||||
&["zz", "z ", " ", " z"],
|
||||
&["::", ".:", "..", ":."],
|
||||
];
|
||||
|
||||
const TOOL_BASE: &[&[&str]] = &[
|
||||
&["/\\", "\\/", "/\\", "\\/"],
|
||||
&["[]", "][", "[]", "]["],
|
||||
&["<>", "><", "<>", "><"],
|
||||
&["**", "x*", "*x", "**"],
|
||||
&["⚙⚙", "⚙⚙", "⚙⚙", "⚙⚙"],
|
||||
];
|
||||
373
codex-rs/tui/src/animations/spinners/sets/animation3.rs
Normal file
373
codex-rs/tui/src/animations/spinners/sets/animation3.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Span;
|
||||
|
||||
use super::SpinnerKind;
|
||||
use super::SpinnerTheme;
|
||||
use crate::animations::spinners::SpinnerStyle;
|
||||
|
||||
const SCALE: usize = 1;
|
||||
const FACE_FRAME_MS: u128 = 840;
|
||||
const FACE_PAUSE_MS: u128 = 1040;
|
||||
const TEXT_MS: u128 = 800;
|
||||
|
||||
static THINKING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static EXPLORING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static EXECUTING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static WAITING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static TOOL_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
|
||||
static THINKING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static EXPLORING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static EXECUTING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static WAITING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static TOOL_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
|
||||
static FACE_SEQUENCES_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static THINKING_DEFS_SCALED: OnceLock<&'static [&'static str]> = OnceLock::new();
|
||||
static EXPLORING_DEFS_SCALED: OnceLock<&'static [&'static str]> = OnceLock::new();
|
||||
static EXECUTING_DEFS_SCALED: OnceLock<&'static [&'static str]> = OnceLock::new();
|
||||
static WAITING_DEFS_SCALED: OnceLock<&'static [&'static str]> = OnceLock::new();
|
||||
static TOOL_DEFS_SCALED: OnceLock<&'static [&'static str]> = OnceLock::new();
|
||||
|
||||
pub(super) fn theme(kind: SpinnerKind) -> SpinnerTheme {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => SpinnerTheme {
|
||||
variants: scaled_thinking(),
|
||||
tick_ms: 150,
|
||||
idle_frame: scaled_idle("._.", &THINKING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Exploring => SpinnerTheme {
|
||||
variants: scaled_exploring(),
|
||||
tick_ms: 120,
|
||||
idle_frame: scaled_idle("o_o", &EXPLORING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Executing => SpinnerTheme {
|
||||
variants: scaled_executing(),
|
||||
tick_ms: 90,
|
||||
idle_frame: scaled_idle("RUN", &EXECUTING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Waiting => SpinnerTheme {
|
||||
variants: scaled_waiting(),
|
||||
tick_ms: 160,
|
||||
idle_frame: scaled_idle("zzz", &WAITING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Tool => SpinnerTheme {
|
||||
variants: scaled_tool(),
|
||||
tick_ms: 120,
|
||||
idle_frame: scaled_idle("[]", &TOOL_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn style_for_kind(kind: SpinnerKind) -> SpinnerStyle {
|
||||
match kind {
|
||||
SpinnerKind::Thinking
|
||||
| SpinnerKind::Exploring
|
||||
| SpinnerKind::Executing
|
||||
| SpinnerKind::Waiting
|
||||
| SpinnerKind::Tool => SpinnerStyle::Fixed(style_default),
|
||||
}
|
||||
}
|
||||
|
||||
fn style_default(frame: &'static str) -> Span<'static> {
|
||||
Span::from(frame).bold()
|
||||
}
|
||||
|
||||
pub(crate) struct Animation3Frame {
|
||||
pub(crate) text: &'static str,
|
||||
pub(crate) face: &'static str,
|
||||
}
|
||||
|
||||
pub(crate) fn frame(
|
||||
kind: SpinnerKind,
|
||||
elapsed_ms: u128,
|
||||
animations_enabled: bool,
|
||||
seed: u64,
|
||||
) -> Animation3Frame {
|
||||
let elapsed_ms = if animations_enabled { elapsed_ms } else { 0 };
|
||||
let definitions = scaled_definitions_for(kind);
|
||||
let text = definitions
|
||||
.get(text_index(elapsed_ms, seed, definitions.len()))
|
||||
.copied()
|
||||
.unwrap_or("...");
|
||||
|
||||
let faces = scaled_face_sequences();
|
||||
let face = faces
|
||||
.get(face_sequence_index(elapsed_ms, seed, faces.len()))
|
||||
.and_then(|seq| seq.get(face_frame_index(elapsed_ms, seq.len())))
|
||||
.copied()
|
||||
.unwrap_or("._.");
|
||||
|
||||
Animation3Frame { text, face }
|
||||
}
|
||||
|
||||
fn scaled_thinking() -> &'static [&'static [&'static str]] {
|
||||
THINKING_SCALED.get_or_init(|| scale_variants(THINKING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_exploring() -> &'static [&'static [&'static str]] {
|
||||
EXPLORING_SCALED.get_or_init(|| scale_variants(EXPLORING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_executing() -> &'static [&'static [&'static str]] {
|
||||
EXECUTING_SCALED.get_or_init(|| scale_variants(EXECUTING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_waiting() -> &'static [&'static [&'static str]] {
|
||||
WAITING_SCALED.get_or_init(|| scale_variants(WAITING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_tool() -> &'static [&'static [&'static str]] {
|
||||
TOOL_SCALED.get_or_init(|| scale_variants(TOOL_BASE))
|
||||
}
|
||||
|
||||
fn scaled_idle(base: &str, cache: &OnceLock<&'static str>) -> &'static str {
|
||||
cache.get_or_init(|| Box::leak(scale_frame(base, SCALE).into_boxed_str()))
|
||||
}
|
||||
|
||||
fn scale_variants(base: &'static [&'static [&'static str]]) -> &'static [&'static [&'static str]] {
|
||||
let scaled: Vec<&'static [&'static str]> = base
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
let frames: Vec<&'static str> = variant
|
||||
.iter()
|
||||
.map(|frame| Box::leak(scale_frame(frame, SCALE).into_boxed_str()) as &'static str)
|
||||
.collect();
|
||||
Box::leak(frames.into_boxed_slice()) as &'static [&'static str]
|
||||
})
|
||||
.collect();
|
||||
Box::leak(scaled.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn scale_definitions(base: &'static [&'static str]) -> &'static [&'static str] {
|
||||
let scaled: Vec<&'static str> = base
|
||||
.iter()
|
||||
.map(|value| {
|
||||
let value = format!("{value}...");
|
||||
Box::leak(scale_frame(&value, SCALE).into_boxed_str()) as &'static str
|
||||
})
|
||||
.collect();
|
||||
Box::leak(scaled.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn scale_frame(frame: &str, scale: usize) -> String {
|
||||
if scale <= 1 {
|
||||
return frame.to_string();
|
||||
}
|
||||
let mut scaled = String::with_capacity(frame.len() * scale);
|
||||
let mut first = true;
|
||||
for ch in frame.chars() {
|
||||
if !first {
|
||||
for _ in 1..scale {
|
||||
scaled.push(' ');
|
||||
}
|
||||
}
|
||||
scaled.push(ch);
|
||||
first = false;
|
||||
}
|
||||
scaled
|
||||
}
|
||||
|
||||
fn scaled_face_sequences() -> &'static [&'static [&'static str]] {
|
||||
FACE_SEQUENCES_SCALED.get_or_init(|| scale_variants(FACE_SEQUENCES))
|
||||
}
|
||||
|
||||
fn scaled_definitions_for(kind: SpinnerKind) -> &'static [&'static str] {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => {
|
||||
THINKING_DEFS_SCALED.get_or_init(|| scale_definitions(THINKING_DEFS))
|
||||
}
|
||||
SpinnerKind::Exploring => {
|
||||
EXPLORING_DEFS_SCALED.get_or_init(|| scale_definitions(EXPLORING_DEFS))
|
||||
}
|
||||
SpinnerKind::Executing => {
|
||||
EXECUTING_DEFS_SCALED.get_or_init(|| scale_definitions(EXECUTING_DEFS))
|
||||
}
|
||||
SpinnerKind::Waiting => WAITING_DEFS_SCALED.get_or_init(|| scale_definitions(WAITING_DEFS)),
|
||||
SpinnerKind::Tool => TOOL_DEFS_SCALED.get_or_init(|| scale_definitions(TOOL_DEFS)),
|
||||
}
|
||||
}
|
||||
|
||||
fn text_index(elapsed_ms: u128, seed: u64, len: usize) -> usize {
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
let step = elapsed_ms / TEXT_MS.max(1);
|
||||
(mix(seed ^ step as u64) as usize) % len
|
||||
}
|
||||
|
||||
fn face_sequence_index(elapsed_ms: u128, seed: u64, len: usize) -> usize {
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
let cycle_ms = FACE_FRAME_MS
|
||||
.saturating_mul(FACE_SEQUENCE_LEN as u128)
|
||||
.saturating_add(FACE_PAUSE_MS);
|
||||
let cycle_idx = elapsed_ms / cycle_ms.max(1);
|
||||
let mut idx = (mix(seed ^ cycle_idx as u64) as usize) % len;
|
||||
if len > 1 {
|
||||
let prev_cycle = cycle_idx.saturating_sub(1);
|
||||
let prev = (mix(seed ^ prev_cycle as u64) as usize) % len;
|
||||
if idx == prev {
|
||||
idx = (idx + 1) % len;
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
fn face_frame_index(elapsed_ms: u128, seq_len: usize) -> usize {
|
||||
if seq_len == 0 {
|
||||
return 0;
|
||||
}
|
||||
let cycle_ms = FACE_FRAME_MS
|
||||
.saturating_mul(seq_len as u128)
|
||||
.saturating_add(FACE_PAUSE_MS);
|
||||
let phase_ms = elapsed_ms % cycle_ms.max(1);
|
||||
let active_ms = FACE_FRAME_MS.saturating_mul(seq_len as u128);
|
||||
if phase_ms < active_ms {
|
||||
return (phase_ms / FACE_FRAME_MS.max(1)) as usize;
|
||||
}
|
||||
seq_len.saturating_sub(1)
|
||||
}
|
||||
|
||||
fn mix(mut value: u64) -> u64 {
|
||||
value ^= value >> 33;
|
||||
value = value.wrapping_mul(0xff51afd7ed558ccd);
|
||||
value ^= value >> 33;
|
||||
value = value.wrapping_mul(0xc4ceb9fe1a85ec53);
|
||||
value ^= value >> 33;
|
||||
value
|
||||
}
|
||||
|
||||
const THINKING_BASE: &[&[&str]] = &[
|
||||
&["._.", "^_^", "^-^", "^o^"],
|
||||
&["o_o", "O_O", "o_O", "O_o"],
|
||||
&["@_@", "x_x", "-_-", "._."],
|
||||
&["#_#", "$_$", "^_^", "._."],
|
||||
&[">_>", "<_<", ">_<", "^_^"],
|
||||
];
|
||||
|
||||
const EXPLORING_BASE: &[&[&str]] = &[
|
||||
&[">_>", "._.", "^-^", "o_o"],
|
||||
&["^_^", "o_o", "O_O", "._."],
|
||||
&["/o\\", "\\o/", "/o\\", "\\o/"],
|
||||
&["[]]", "[]<", "<[]>", "[][]"],
|
||||
&["^w^", "^_^", "._.", "!_!"],
|
||||
];
|
||||
|
||||
const EXECUTING_BASE: &[&[&str]] = &[
|
||||
&["RUN", "PUSH", "SHIP", "DONE"],
|
||||
&["DO!", "GO!", "NOW", "YEP"],
|
||||
&["MOVE", "JUMP", "ROLL", "FLIP"],
|
||||
&["SYNC", "MERG", "COMM", "PUSH"],
|
||||
&["BOOT", "TEST", "LINT", "PASS"],
|
||||
];
|
||||
|
||||
const WAITING_BASE: &[&[&str]] = &[
|
||||
&["zzz", "z z", " z", "z "],
|
||||
&["...", ".. ", ". ", " .."],
|
||||
&["T_T", "-_-", "._.", "o_o"],
|
||||
&["WAIT", "HOLD", "PAUS", "REST"],
|
||||
&["SIGH", "HMM", "UMM", "OK?"],
|
||||
];
|
||||
|
||||
const TOOL_BASE: &[&[&str]] = &[
|
||||
&["[]", "][", "[]", "]["],
|
||||
&["< >", "><", "<>", "><"],
|
||||
&["/\\", "\\/", "/\\", "\\/"],
|
||||
&["TOOL", "WREN", "PICK", "FIX!"],
|
||||
&["CUT", "COPY", "PAST", "DONE"],
|
||||
];
|
||||
|
||||
const FACE_SEQUENCE_LEN: usize = 3;
|
||||
const FACE_SEQUENCES: &[&[&str]] = &[
|
||||
&["._.", "^_^", "^-^"],
|
||||
&["^-^", "^_^", "^o^"],
|
||||
&["^_^", "o_o", "O_O"],
|
||||
&["o_o", "O_o", "o_O"],
|
||||
&["o_O", "@_@", "x_x"],
|
||||
&["x_x", "-_-", "._."],
|
||||
&["._.", "-_-", ">_>"],
|
||||
&[">_>", "<_<", ">_<"],
|
||||
&[">_<", "^_^", "-_-"],
|
||||
&["#_#", "^_^", "._."],
|
||||
&["$_$", "o_O", "._."],
|
||||
&["._.", "._.", "^_^"],
|
||||
&["#_#", "^_^", "^.^"],
|
||||
&["^.^", "^_^", "^-^"],
|
||||
&["^_^", "T_T", "^_^"],
|
||||
&["^_^", "@_@", "^_^"],
|
||||
&["0_0", "o_o", "O_O"],
|
||||
&["O_O", "o_o", "._."],
|
||||
&["^_^", "^-^", "^o^"],
|
||||
&["O_O", "^w^", "^_^"],
|
||||
&["._.", "!_!", "^_^"],
|
||||
&["-_-", "T_T", "._."],
|
||||
&["@_@", "0_0", "o_o"],
|
||||
&[">_>", "._.", "^-^"],
|
||||
&["o_o", "._.", "^_^"],
|
||||
];
|
||||
|
||||
const THINKING_DEFS: &[&str] = &[
|
||||
"Boop the Bit",
|
||||
"Pet the Bit",
|
||||
"Flip the Bit",
|
||||
"Debug a Bit",
|
||||
"Cache a Bit",
|
||||
"Weep a Bit",
|
||||
"Smile a Bit",
|
||||
"Sigh then Submit",
|
||||
"Zen Commit",
|
||||
"Sad Commit",
|
||||
"Glad Commit",
|
||||
"Emit",
|
||||
"Admit",
|
||||
"Omit",
|
||||
];
|
||||
|
||||
const EXPLORING_DEFS: &[&str] = &[
|
||||
"Nudge the Git",
|
||||
"Curse the Git",
|
||||
"Bless the Git",
|
||||
"Tame the JIT",
|
||||
"Blame the JIT",
|
||||
"Merge then Commit",
|
||||
"Push and Commit",
|
||||
"Pull then Commit",
|
||||
"Patch then Commit",
|
||||
];
|
||||
|
||||
const EXECUTING_DEFS: &[&str] = &[
|
||||
"Test and Submit",
|
||||
"Lint and Submit",
|
||||
"Merge then Commit",
|
||||
"Push and Commit",
|
||||
"Patch then Commit",
|
||||
"Panic Commit",
|
||||
"Debug a Bit",
|
||||
];
|
||||
|
||||
const WAITING_DEFS: &[&str] = &[
|
||||
"Weep a Bit",
|
||||
"Sigh then Submit",
|
||||
"Sad Commit",
|
||||
"Zen Commit",
|
||||
"Glad Commit",
|
||||
"Omit",
|
||||
];
|
||||
|
||||
const TOOL_DEFS: &[&str] = &[
|
||||
"Debug a Bit",
|
||||
"Cache a Bit",
|
||||
"Tame the JIT",
|
||||
"Blame the JIT",
|
||||
"Patch then Commit",
|
||||
"Merge then Commit",
|
||||
];
|
||||
423
codex-rs/tui/src/animations/spinners/sets/animation4.rs
Normal file
423
codex-rs/tui/src/animations/spinners/sets/animation4.rs
Normal file
@@ -0,0 +1,423 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Span;
|
||||
|
||||
use super::SpinnerKind;
|
||||
use super::SpinnerTheme;
|
||||
use crate::animations::spinners::SpinnerStyle;
|
||||
|
||||
const SCALE: usize = 1;
|
||||
const TEXT_MS: u128 = 1400;
|
||||
|
||||
static THINKING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static EXPLORING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static EXECUTING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static WAITING_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static TOOL_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
|
||||
static THINKING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static EXPLORING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static EXECUTING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static WAITING_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
static TOOL_IDLE: OnceLock<&'static str> = OnceLock::new();
|
||||
|
||||
static FACE_SEQUENCES_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
static DEFINITION_ARCS_SCALED: OnceLock<&'static [&'static [&'static str]]> = OnceLock::new();
|
||||
|
||||
pub(super) fn theme(kind: SpinnerKind) -> SpinnerTheme {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => SpinnerTheme {
|
||||
variants: scaled_thinking(),
|
||||
tick_ms: 150,
|
||||
idle_frame: scaled_idle("._.", &THINKING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Exploring => SpinnerTheme {
|
||||
variants: scaled_exploring(),
|
||||
tick_ms: 120,
|
||||
idle_frame: scaled_idle("o_o", &EXPLORING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Executing => SpinnerTheme {
|
||||
variants: scaled_executing(),
|
||||
tick_ms: 90,
|
||||
idle_frame: scaled_idle("RUN", &EXECUTING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Waiting => SpinnerTheme {
|
||||
variants: scaled_waiting(),
|
||||
tick_ms: 160,
|
||||
idle_frame: scaled_idle("zzz", &WAITING_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
SpinnerKind::Tool => SpinnerTheme {
|
||||
variants: scaled_tool(),
|
||||
tick_ms: 120,
|
||||
idle_frame: scaled_idle("[]", &TOOL_IDLE),
|
||||
style: style_for_kind(kind),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn style_for_kind(kind: SpinnerKind) -> SpinnerStyle {
|
||||
match kind {
|
||||
SpinnerKind::Thinking
|
||||
| SpinnerKind::Exploring
|
||||
| SpinnerKind::Executing
|
||||
| SpinnerKind::Waiting
|
||||
| SpinnerKind::Tool => SpinnerStyle::Fixed(style_default),
|
||||
}
|
||||
}
|
||||
|
||||
fn style_default(frame: &'static str) -> Span<'static> {
|
||||
Span::from(frame).bold()
|
||||
}
|
||||
|
||||
pub(crate) struct Animation4Frame {
|
||||
pub(crate) text: &'static str,
|
||||
pub(crate) face: &'static str,
|
||||
}
|
||||
|
||||
pub(crate) fn frame(
|
||||
_kind: SpinnerKind,
|
||||
elapsed_ms: u128,
|
||||
animations_enabled: bool,
|
||||
seed: u64,
|
||||
) -> Animation4Frame {
|
||||
let elapsed_ms = if animations_enabled { elapsed_ms } else { 0 };
|
||||
let text_ms = jittered_text_ms(seed);
|
||||
let step = elapsed_ms / text_ms.max(1);
|
||||
let step = step.saturating_add(start_offset_step(seed, scaled_definition_arcs()));
|
||||
let faces = scaled_face_sequences();
|
||||
let arcs = scaled_definition_arcs();
|
||||
let (arc_idx, arc_step_offset) = arc_index(step, arcs);
|
||||
let text = arcs
|
||||
.get(arc_idx)
|
||||
.and_then(|arc| arc.get(text_index(arc_step_offset, arc.len())))
|
||||
.copied()
|
||||
.unwrap_or("Ok, focus...");
|
||||
let face = faces
|
||||
.get(face_sequence_index(step, seed, faces.len()))
|
||||
.and_then(|seq| seq.get(face_frame_index(arc_step_offset, seq.len())))
|
||||
.copied()
|
||||
.unwrap_or("._.");
|
||||
|
||||
Animation4Frame { text, face }
|
||||
}
|
||||
|
||||
fn scaled_thinking() -> &'static [&'static [&'static str]] {
|
||||
THINKING_SCALED.get_or_init(|| scale_variants(THINKING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_exploring() -> &'static [&'static [&'static str]] {
|
||||
EXPLORING_SCALED.get_or_init(|| scale_variants(EXPLORING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_executing() -> &'static [&'static [&'static str]] {
|
||||
EXECUTING_SCALED.get_or_init(|| scale_variants(EXECUTING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_waiting() -> &'static [&'static [&'static str]] {
|
||||
WAITING_SCALED.get_or_init(|| scale_variants(WAITING_BASE))
|
||||
}
|
||||
|
||||
fn scaled_tool() -> &'static [&'static [&'static str]] {
|
||||
TOOL_SCALED.get_or_init(|| scale_variants(TOOL_BASE))
|
||||
}
|
||||
|
||||
fn scaled_idle(base: &str, cache: &OnceLock<&'static str>) -> &'static str {
|
||||
cache.get_or_init(|| Box::leak(scale_frame(base, SCALE).into_boxed_str()))
|
||||
}
|
||||
|
||||
fn scale_variants(base: &'static [&'static [&'static str]]) -> &'static [&'static [&'static str]] {
|
||||
let scaled: Vec<&'static [&'static str]> = base
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
let frames: Vec<&'static str> = variant
|
||||
.iter()
|
||||
.map(|frame| Box::leak(scale_frame(frame, SCALE).into_boxed_str()) as &'static str)
|
||||
.collect();
|
||||
Box::leak(frames.into_boxed_slice()) as &'static [&'static str]
|
||||
})
|
||||
.collect();
|
||||
Box::leak(scaled.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn scale_definition_arcs(
|
||||
base: &'static [&'static [&'static str]],
|
||||
) -> &'static [&'static [&'static str]] {
|
||||
let scaled: Vec<&'static [&'static str]> = base
|
||||
.iter()
|
||||
.map(|arc| {
|
||||
let lines: Vec<&'static str> = arc
|
||||
.iter()
|
||||
.map(|line| Box::leak(scale_frame(line, SCALE).into_boxed_str()) as &'static str)
|
||||
.collect();
|
||||
Box::leak(lines.into_boxed_slice()) as &'static [&'static str]
|
||||
})
|
||||
.collect();
|
||||
Box::leak(scaled.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn scale_frame(frame: &str, scale: usize) -> String {
|
||||
if scale <= 1 {
|
||||
return frame.to_string();
|
||||
}
|
||||
let mut scaled = String::with_capacity(frame.len() * scale);
|
||||
let mut first = true;
|
||||
for ch in frame.chars() {
|
||||
if !first {
|
||||
for _ in 1..scale {
|
||||
scaled.push(' ');
|
||||
}
|
||||
}
|
||||
scaled.push(ch);
|
||||
first = false;
|
||||
}
|
||||
scaled
|
||||
}
|
||||
|
||||
fn scaled_face_sequences() -> &'static [&'static [&'static str]] {
|
||||
FACE_SEQUENCES_SCALED.get_or_init(|| scale_variants(FACE_SEQUENCES))
|
||||
}
|
||||
|
||||
fn scaled_definition_arcs() -> &'static [&'static [&'static str]] {
|
||||
DEFINITION_ARCS_SCALED.get_or_init(|| scale_definition_arcs(DEFINITION_ARCS))
|
||||
}
|
||||
|
||||
fn arc_index(step: u128, arcs: &[&[&str]]) -> (usize, u128) {
|
||||
if arcs.is_empty() {
|
||||
return (0, 0);
|
||||
}
|
||||
let mut remaining = step;
|
||||
for (idx, arc) in arcs.iter().enumerate() {
|
||||
let arc_len = arc.len().max(1) as u128;
|
||||
if remaining < arc_len {
|
||||
return (idx, remaining);
|
||||
}
|
||||
remaining = remaining.saturating_sub(arc_len);
|
||||
}
|
||||
((step as usize) % arcs.len(), 0)
|
||||
}
|
||||
|
||||
fn text_index(arc_step_offset: u128, len: usize) -> usize {
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
(arc_step_offset as usize) % len
|
||||
}
|
||||
|
||||
fn jittered_text_ms(seed: u64) -> u128 {
|
||||
let percent = 25 + (mix(seed) % 125) as u128;
|
||||
TEXT_MS.saturating_mul(percent).max(1) / 100
|
||||
}
|
||||
|
||||
fn start_offset_step(seed: u64, arcs: &[&[&str]]) -> u128 {
|
||||
let total = total_steps(arcs);
|
||||
if total == 0 {
|
||||
return 0;
|
||||
}
|
||||
let offset = mix(seed) % total as u64;
|
||||
offset as u128
|
||||
}
|
||||
|
||||
fn total_steps(arcs: &[&[&str]]) -> u128 {
|
||||
arcs.iter().map(|arc| arc.len() as u128).sum()
|
||||
}
|
||||
|
||||
fn face_sequence_index(step: u128, seed: u64, len: usize) -> usize {
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut idx = (mix(seed ^ step as u64) as usize) % len;
|
||||
if len > 1 {
|
||||
let prev_step = step.saturating_sub(1);
|
||||
let prev = (mix(seed ^ prev_step as u64) as usize) % len;
|
||||
if idx == prev {
|
||||
idx = (idx + 1) % len;
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
fn face_frame_index(arc_step_offset: u128, seq_len: usize) -> usize {
|
||||
if seq_len == 0 {
|
||||
return 0;
|
||||
}
|
||||
(arc_step_offset as usize) % seq_len
|
||||
}
|
||||
|
||||
fn mix(mut value: u64) -> u64 {
|
||||
value ^= value >> 33;
|
||||
value = value.wrapping_mul(0xff51afd7ed558ccd);
|
||||
value ^= value >> 33;
|
||||
value = value.wrapping_mul(0xc4ceb9fe1a85ec53);
|
||||
value ^= value >> 33;
|
||||
value
|
||||
}
|
||||
|
||||
const THINKING_BASE: &[&[&str]] = &[
|
||||
&["._.", "^_^", "^-^", "#_#"],
|
||||
&["o_o", "O_O", "o_O", "O_o"],
|
||||
&["o_o", "x_x", "-_-", "._."],
|
||||
&["#_#", "$_$", "^_^", "._."],
|
||||
&[">_>", "<_<", ">_<", "^_^"],
|
||||
];
|
||||
|
||||
const EXPLORING_BASE: &[&[&str]] = &[
|
||||
&[">_>", "._.", "^-^", "o_o"],
|
||||
&["^_^", "o_o", "O_O", "._."],
|
||||
&["/o\\", "\\o/", "/o\\", "\\o/"],
|
||||
&["^w^", "^_^", "._.", "!_!"],
|
||||
];
|
||||
|
||||
const EXECUTING_BASE: &[&[&str]] = &[
|
||||
&["RUN", "PUSH", "SHIP", "DONE"],
|
||||
&["DO!", "GO!", "NOW", "YEP"],
|
||||
&["MOVE", "JUMP", "ROLL", "FLIP"],
|
||||
&["SYNC", "MERG", "COMM", "PUSH"],
|
||||
&["BOOT", "TEST", "LINT", "PASS"],
|
||||
];
|
||||
|
||||
const WAITING_BASE: &[&[&str]] = &[
|
||||
&["zzz", "z z", " z", "z "],
|
||||
&["...", ".. ", ". ", " .."],
|
||||
&["T_T", "-_-", "._.", "o_o"],
|
||||
&["WAIT", "HOLD", "PAUS", "REST"],
|
||||
&["SIGH", "HMM", "UMM", "OK?"],
|
||||
];
|
||||
|
||||
const TOOL_BASE: &[&[&str]] = &[
|
||||
&["[]", "][", "[]", "]["],
|
||||
&["< >", "><", "<>", "><"],
|
||||
&["/\\", "\\/", "/\\", "\\/"],
|
||||
&["TOOL", "WREN", "PICK", "FIX!"],
|
||||
&["CUT", "COPY", "PAST", "DONE"],
|
||||
];
|
||||
|
||||
const FACE_SEQUENCES: &[&[&str]] = &[
|
||||
&["._.", "^_^", "^-^"],
|
||||
&["^-^", "^_^", "^o^"],
|
||||
&["^_^", "o_o", "O_O"],
|
||||
&["o_o", "O_o", "o_O"],
|
||||
&["o_O", "@_@", "x_x"],
|
||||
&["x_x", "-_-", "._."],
|
||||
&["._.", "-_-", ">_>"],
|
||||
&[">_>", "<_<", ">_<"],
|
||||
&[">_<", "^_^", "-_-"],
|
||||
&["#_#", "^_^", "._."],
|
||||
&["$_$", "o_O", "._."],
|
||||
&["._.", "._.", "^_^"],
|
||||
&["#_#", "^_^", "^.^"],
|
||||
&["^.^", "^_^", "^-^"],
|
||||
&["^_^", "T_T", "^_^"],
|
||||
&["^_^", "@_@", "^_^"],
|
||||
&["0_0", "o_o", "O_O"],
|
||||
&["O_O", "o_o", "._."],
|
||||
&["^_^", "^-^", "^o^"],
|
||||
&["O_O", "^w^", "^_^"],
|
||||
&["._.", "!_!", "^_^"],
|
||||
&["-_-", "T_T", "._."],
|
||||
&["@_@", "0_0", "o_o"],
|
||||
&[">_>", "._.", "^-^"],
|
||||
&["o_o", "._.", "^_^"],
|
||||
];
|
||||
|
||||
const DEFINITION_ARCS: &[&[&str]] = &[
|
||||
&[
|
||||
"And now, the moment.",
|
||||
"I am doing the thing.",
|
||||
"On that stubborn page.",
|
||||
"To calm the spinner.",
|
||||
"With one better check.",
|
||||
"And one sweeter line.",
|
||||
"Here we go again.",
|
||||
"For real this time.",
|
||||
],
|
||||
&[
|
||||
"No more looping.",
|
||||
"No more coping.",
|
||||
"Promise.",
|
||||
"Pinky swear.",
|
||||
"Cross my heart.",
|
||||
"If it loops, I'll cry.",
|
||||
"If it works, I'll fly.",
|
||||
"Ok, focus.",
|
||||
],
|
||||
&[
|
||||
"Starting vibes...",
|
||||
"Starting logic...",
|
||||
"Starting regret...",
|
||||
"Spinning politely.",
|
||||
"Caching bravely.",
|
||||
"Fetching gently.",
|
||||
"Retrying softly.",
|
||||
"Still retrying.",
|
||||
],
|
||||
&[
|
||||
"This is fine.",
|
||||
"This is code.",
|
||||
"This is hope.",
|
||||
"This is rope.",
|
||||
"Tugging the thread.",
|
||||
"Oops, it's dread.",
|
||||
"Kidding. Mostly.",
|
||||
],
|
||||
&[
|
||||
"Compiling courage.",
|
||||
"Linking feelings.",
|
||||
"Bundling dreams.",
|
||||
"Shipping screams.",
|
||||
"Hydrating hopes.",
|
||||
"Revalidating jokes.",
|
||||
],
|
||||
&[
|
||||
"Negotiating with React.",
|
||||
"Begging the router.",
|
||||
"Asking state nicely.",
|
||||
"State said \"no.\"",
|
||||
"State said \"lol.\"",
|
||||
"Ok that's rude.",
|
||||
],
|
||||
&[
|
||||
"Back to build.",
|
||||
"Build is life.",
|
||||
"Build is love.",
|
||||
"Build is joy.",
|
||||
],
|
||||
&[
|
||||
"No more looping.",
|
||||
"No more snooping.",
|
||||
"No more duping.",
|
||||
"Serious promise.",
|
||||
"Serious-serious.",
|
||||
"Double pinky.",
|
||||
"Triple pinky.",
|
||||
"Tap the keyboard.",
|
||||
"Seal the commit.",
|
||||
"Ok I'm calm.",
|
||||
"I'm not calm.",
|
||||
"I'm calm again.",
|
||||
],
|
||||
&[
|
||||
"Optimism loaded.",
|
||||
"Optimism unloaded.",
|
||||
"Joy is async.",
|
||||
"Sadness is sync.",
|
||||
"Hope is pending.",
|
||||
"Dread is trending.",
|
||||
"It passed locally.",
|
||||
"Eventually.",
|
||||
"I trust the tests.",
|
||||
"The tests hate me.",
|
||||
"Ok that got dark.",
|
||||
"Ok that got funny.",
|
||||
],
|
||||
&[
|
||||
"Back to coding.",
|
||||
"Coding is light.",
|
||||
"Coding is life.",
|
||||
"Coding is joy.",
|
||||
],
|
||||
];
|
||||
67
codex-rs/tui/src/animations/spinners/sets/default.rs
Normal file
67
codex-rs/tui/src/animations/spinners/sets/default.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use super::SpinnerKind;
|
||||
use super::SpinnerTheme;
|
||||
use crate::animations::spinners::SpinnerStyle;
|
||||
use crate::animations::spinners::style_executing;
|
||||
use crate::animations::spinners::style_exploring;
|
||||
use crate::animations::spinners::style_thinking;
|
||||
use crate::animations::spinners::style_tool;
|
||||
use crate::animations::spinners::style_waiting;
|
||||
|
||||
pub(super) fn theme(kind: SpinnerKind) -> SpinnerTheme {
|
||||
match kind {
|
||||
SpinnerKind::Thinking => SpinnerTheme {
|
||||
variants: THINKING_VARIANTS,
|
||||
tick_ms: 200,
|
||||
idle_frame: "....",
|
||||
style: SpinnerStyle::Fixed(style_thinking),
|
||||
},
|
||||
SpinnerKind::Exploring => SpinnerTheme {
|
||||
variants: EXPLORING_VARIANTS,
|
||||
tick_ms: 140,
|
||||
idle_frame: "....",
|
||||
style: SpinnerStyle::Fixed(style_exploring),
|
||||
},
|
||||
SpinnerKind::Executing => SpinnerTheme {
|
||||
variants: EXECUTING_VARIANTS,
|
||||
tick_ms: 120,
|
||||
idle_frame: "====",
|
||||
style: SpinnerStyle::Fixed(style_executing),
|
||||
},
|
||||
SpinnerKind::Waiting => SpinnerTheme {
|
||||
variants: WAITING_VARIANTS,
|
||||
tick_ms: 220,
|
||||
idle_frame: "....",
|
||||
style: SpinnerStyle::Fixed(style_waiting),
|
||||
},
|
||||
SpinnerKind::Tool => SpinnerTheme {
|
||||
variants: TOOL_VARIANTS,
|
||||
tick_ms: 160,
|
||||
idle_frame: "[--]",
|
||||
style: SpinnerStyle::Fixed(style_tool),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const THINKING_VARIANTS: &[&[&str]] = &[
|
||||
&["o...", ".o..", "..o.", "...o"],
|
||||
&[".o..", "..o.", "...o", "o..."],
|
||||
];
|
||||
|
||||
const EXPLORING_VARIANTS: &[&[&str]] = &[
|
||||
&[">...", ".>..", "..>.", "...>", "..>.", ".>.."],
|
||||
&["<...", ".<..", "..<.", "...<", "..<.", ".<.."],
|
||||
];
|
||||
|
||||
const EXECUTING_VARIANTS: &[&[&str]] = &[&[
|
||||
"====", "-===", "--==", "---=", "----", "=---", "==--", "===-",
|
||||
]];
|
||||
|
||||
const WAITING_VARIANTS: &[&[&str]] = &[
|
||||
&[". . ", " .. ", " . ", " .. "],
|
||||
&["....", " .. ", "....", " .. "],
|
||||
];
|
||||
|
||||
const TOOL_VARIANTS: &[&[&str]] = &[
|
||||
&["[##]", "[# ]", "[ #]", "[##]"],
|
||||
&["[<>]", "[><]", "[<>]", "[><]"],
|
||||
];
|
||||
19
codex-rs/tui/src/animations/spinners/sets/mod.rs
Normal file
19
codex-rs/tui/src/animations/spinners/sets/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use super::SpinnerKind;
|
||||
use super::SpinnerSet;
|
||||
use super::SpinnerTheme;
|
||||
|
||||
mod animation1;
|
||||
mod animation2;
|
||||
pub(crate) mod animation3;
|
||||
pub(crate) mod animation4;
|
||||
mod default;
|
||||
|
||||
pub(super) fn theme(set: SpinnerSet, kind: SpinnerKind) -> SpinnerTheme {
|
||||
match set {
|
||||
SpinnerSet::Default => default::theme(kind),
|
||||
SpinnerSet::Animation1 => animation1::theme(kind),
|
||||
SpinnerSet::Animation2 => animation2::theme(kind),
|
||||
SpinnerSet::Animation3 => animation3::theme(kind),
|
||||
SpinnerSet::Animation4 => animation4::theme(kind),
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: blob1
|
||||
expression: active_blob(&chat)
|
||||
---
|
||||
• Exploring
|
||||
>... Exploring
|
||||
└ List ls -la
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: blob3
|
||||
expression: active_blob(&chat)
|
||||
---
|
||||
• Exploring
|
||||
<... Exploring
|
||||
└ List ls -la
|
||||
Read foo.txt
|
||||
|
||||
@@ -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,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,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})
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
source: tui/src/status_indicator_widget.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"• Working (0s • esc "
|
||||
"o... Working (0s • e"
|
||||
" "
|
||||
|
||||
@@ -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) "
|
||||
" "
|
||||
|
||||
@@ -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