Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6889a10
Revert "Revert "Streaming markdown (#1920)" (#1981)"
easong-openai Aug 8, 2025
eb6f811
better tests, approval UI fix
easong-openai Aug 8, 2025
e906e35
tests
easong-openai Aug 8, 2025
fd87fc1
fixtures, working pretty well
easong-openai Aug 9, 2025
8f2ef81
fmt, clippy
easong-openai Aug 9, 2025
44a7e01
cleanup
easong-openai Aug 9, 2025
b080dc9
refactor
easong-openai Aug 9, 2025
39b04d7
pulling things out of chatwidget
easong-openai Aug 9, 2025
8f187ed
cleanup
easong-openai Aug 9, 2025
ba3f13f
fmt, clippy
easong-openai Aug 9, 2025
7ba2eda
merge
easong-openai Aug 9, 2025
eb29dd6
refactor?
easong-openai Aug 9, 2025
194e735
cleanup?
easong-openai Aug 9, 2025
ffbab2b
tui tests: fix vt100_replay_binary_size_session_from_log\n- Use asser…
easong-openai Aug 10, 2025
2c6c0a8
fix cutoff
easong-openai Aug 10, 2025
f170ed5
tui tests: fix clippy uninlined_format_args in chatwidget tests
easong-openai Aug 10, 2025
d855226
clippy
easong-openai Aug 10, 2025
437709e
fmt
easong-openai Aug 10, 2025
50be6fa
fmt
easong-openai Aug 11, 2025
86d5e6a
fix ordering
easong-openai Aug 11, 2025
4fca777
fmt
easong-openai Aug 11, 2025
3b554c4
merge
easong-openai Aug 11, 2025
b689f2d
merge
easong-openai Aug 11, 2025
7654edf
codespell
easong-openai Aug 11, 2025
76c5d01
cleanup
easong-openai Aug 12, 2025
79b4a2c
tui: make binary_size_transcript_matches_ideal_fixture robust to CI p…
easong-openai Aug 12, 2025
c984b13
cleanup
easong-openai Aug 12, 2025
719de21
clippy
easong-openai Aug 12, 2025
2e24c90
merge
easong-openai Aug 12, 2025
5323cc7
windows
easong-openai Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codespellrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl
check-hidden = true
ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b
ignore-words-list = ratatui,ser
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions codex-rs/core/src/chat_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ where
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(_)))) => {
continue;
}
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded))) => {
continue;
}
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,18 @@ async fn process_sse<S>(
| "response.function_call_arguments.delta"
| "response.in_progress"
| "response.output_item.added"
| "response.output_text.done"
| "response.reasoning_summary_part.added"
| "response.reasoning_summary_text.done" => {
// Currently, we ignore these events, but we handle them
| "response.output_text.done" => {
// Currently, we ignore this event, but we handle it
// separately to skip the logging message in the `other` case.
}
"response.reasoning_summary_part.added" => {
// Boundary between reasoning summary sections (e.g., titles).
let event = ResponseEvent::ReasoningSummaryPartAdded;
if tx_event.send(Ok(event)).await.is_err() {
return;
}
}
"response.reasoning_summary_text.done" => {}
other => debug!(other, "sse event"),
}
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/client_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ pub enum ResponseEvent {
OutputTextDelta(String),
ReasoningSummaryDelta(String),
ReasoningContentDelta(String),
ReasoningSummaryPartAdded,
}

#[derive(Debug, Serialize)]
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ use crate::protocol::AgentReasoningDeltaEvent;
use crate::protocol::AgentReasoningEvent;
use crate::protocol::AgentReasoningRawContentDeltaEvent;
use crate::protocol::AgentReasoningRawContentEvent;
use crate::protocol::AgentReasoningSectionBreakEvent;
use crate::protocol::ApplyPatchApprovalRequestEvent;
use crate::protocol::AskForApproval;
use crate::protocol::BackgroundEventEvent;
Expand Down Expand Up @@ -1477,6 +1478,13 @@ async fn try_run_turn(
};
sess.tx_event.send(event).await.ok();
}
ResponseEvent::ReasoningSummaryPartAdded => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this because we get multiple reasoning sections is a row?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just because we don't want to track content_part -> Thinking transition?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a way to affirmatively break between different parts of the summary presently so that we can make sure they get newlines. I also want it so that we can just show thinking headers in the UI, similar to how we show a summarized command view - this will be important for the next part of the UI overhaul.

let event = Event {
id: sub_id.to_string(),
msg: EventMsg::AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent {}),
};
sess.tx_event.send(event).await.ok();
}
ResponseEvent::ReasoningContentDelta(delta) => {
if sess.show_raw_agent_reasoning {
let event = Event {
Expand Down
8 changes: 6 additions & 2 deletions codex-rs/core/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,7 @@ impl SandboxPolicy {
}
}

/// Always returns `true` for now, as we do not yet support restricting read
/// access.
/// Always returns `true`; restricting read access is not supported.
pub fn has_full_disk_read_access(&self) -> bool {
true
}
Expand Down Expand Up @@ -384,6 +383,8 @@ pub enum EventMsg {

/// Agent reasoning content delta event from agent.
AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
/// Signaled when the model begins a new reasoning summary section (e.g., a new titled block).
AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),

/// Ack the client's configure message.
SessionConfigured(SessionConfiguredEvent),
Expand Down Expand Up @@ -531,6 +532,9 @@ pub struct AgentReasoningRawContentDeltaEvent {
pub delta: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentReasoningSectionBreakEvent {}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentReasoningDeltaEvent {
pub delta: String,
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/tests/common/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ pub async fn wait_for_event_with_timeout<F>(
where
F: FnMut(&codex_core::protocol::EventMsg) -> bool,
{
use tokio::time::Duration;
use tokio::time::timeout;
loop {
let ev = timeout(wait_time, codex.next_event())
// Allow a bit more time to accommodate async startup work (e.g. config IO, tool discovery)
let ev = timeout(wait_time.max(Duration::from_secs(5)), codex.next_event())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we up the default above?

.await
.expect("timeout waiting for event")
.expect("stream ended unexpectedly");
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/exec/src/event_processor_with_human_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ impl EventProcessor for EventProcessorWithHumanOutput {
#[allow(clippy::expect_used)]
std::io::stdout().flush().expect("could not flush stdout");
}
EventMsg::AgentReasoningSectionBreak(_) => {
if !self.show_agent_reasoning {
return CodexStatus::Running;
}
println!();
#[allow(clippy::expect_used)]
std::io::stdout().flush().expect("could not flush stdout");
}
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => {
if !self.show_raw_agent_reasoning {
return CodexStatus::Running;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/mcp-server/src/codex_tool_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ async fn run_codex_tool_session_inner(
| EventMsg::TaskStarted
| EventMsg::TokenCount(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningSectionBreak(_)
| EventMsg::McpToolCallBegin(_)
| EventMsg::McpToolCallEnd(_)
| EventMsg::ExecCommandBegin(_)
Expand Down
1 change: 1 addition & 0 deletions codex-rs/mcp-server/src/conversation_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub async fn run_conversation_loop(
| EventMsg::TaskStarted
| EventMsg::TokenCount(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningSectionBreak(_)
| EventMsg::McpToolCallBegin(_)
| EventMsg::McpToolCallEnd(_)
| EventMsg::ExecCommandBegin(_)
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ path = "src/lib.rs"
[features]
# Enable vt100-based tests (emulator) when running with `--features vt100-tests`.
vt100-tests = []
# Gate verbose debug logging inside the TUI implementation.
debug-logs = []

[lints]
workspace = true
Expand All @@ -39,6 +41,7 @@ crossterm = { version = "0.28.1", features = ["bracketed-paste"] }
diffy = "0.4.2"
image = { version = "^0.25.6", default-features = false, features = ["jpeg"] }
lazy_static = "1"
once_cell = "1"
mcp-types = { path = "../mcp-types" }
path-clean = "1.0.1"
ratatui = { version = "0.29.0", features = [
Expand Down
42 changes: 34 additions & 8 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use std::thread;
use std::time::Duration;

/// Time window for debouncing redraw requests.
const REDRAW_DEBOUNCE: Duration = Duration::from_millis(10);
const REDRAW_DEBOUNCE: Duration = Duration::from_millis(1);

/// Top-level application state: which full-screen view is currently active.
#[allow(clippy::large_enum_variant)]
Expand Down Expand Up @@ -63,6 +63,9 @@ pub(crate) struct App<'a> {
pending_history_lines: Vec<Line<'static>>,

enhanced_keys_supported: bool,

/// Controls the animation thread that sends CommitTick events.
commit_anim_running: Arc<AtomicBool>,
}

/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
Expand Down Expand Up @@ -110,10 +113,8 @@ impl App<'_> {
app_event_tx.send(AppEvent::RequestRedraw);
}
crossterm::event::Event::Paste(pasted) => {
// Many terminals convert newlines to \r when
// pasting, e.g. [iTerm2][]. But [tui-textarea
// expects \n][tui-textarea]. This seems like a bug
// in tui-textarea IMO, but work around it for now.
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
// but tui-textarea expects \n. Normalize CR to LF.
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
let pasted = pasted.replace("\r", "\n");
Expand Down Expand Up @@ -172,6 +173,7 @@ impl App<'_> {
file_search,
pending_redraw,
enhanced_keys_supported,
commit_anim_running: Arc::new(AtomicBool::new(false)),
}
}

Expand All @@ -188,7 +190,7 @@ impl App<'_> {
// redraw is already pending so we can return early.
if self
.pending_redraw
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return;
Expand All @@ -199,7 +201,7 @@ impl App<'_> {
thread::spawn(move || {
thread::sleep(REDRAW_DEBOUNCE);
tx.send(AppEvent::Redraw);
pending_redraw.store(false, Ordering::SeqCst);
pending_redraw.store(false, Ordering::Release);
});
}

Expand All @@ -220,6 +222,30 @@ impl App<'_> {
AppEvent::Redraw => {
std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??;
}
AppEvent::StartCommitAnimation => {
if self
.commit_anim_running
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
let tx = self.app_event_tx.clone();
let running = self.commit_anim_running.clone();
thread::spawn(move || {
while running.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(50));
tx.send(AppEvent::CommitTick);
}
});
}
}
AppEvent::StopCommitAnimation => {
self.commit_anim_running.store(false, Ordering::Release);
}
AppEvent::CommitTick => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.on_commit_tick();
}
}
AppEvent::KeyEvent(key_event) => {
match key_event {
KeyEvent {
Expand Down Expand Up @@ -288,7 +314,7 @@ impl App<'_> {
self.dispatch_key_event(key_event);
}
_ => {
// Ignore Release key events for now.
// Ignore Release key events.
}
};
}
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::app::ChatWidgetArgs;
use crate::slash_command::SlashCommand;

#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub(crate) enum AppEvent {
CodexEvent(Event),

Expand Down Expand Up @@ -50,6 +51,10 @@ pub(crate) enum AppEvent {

InsertHistory(Vec<Line<'static>>),

StartCommitAnimation,
StopCommitAnimation,
CommitTick,

/// Onboarding: result of login_with_chatgpt.
OnboardingAuthComplete(Result<(), String>),
OnboardingComplete(ChatWidgetArgs),
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui/src/app_event_sender.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::mpsc::Sender;

use crate::app_event::AppEvent;
use crate::session_log;

#[derive(Clone, Debug)]
pub(crate) struct AppEventSender {
Expand All @@ -15,6 +16,11 @@ impl AppEventSender {
/// Send an event to the app event channel. If it fails, we swallow the
/// error and log it.
pub(crate) fn send(&self, event: AppEvent) {
// Record inbound events for high-fidelity session replay.
// Avoid double-logging Ops; those are logged at the point of submission.
if !matches!(event, AppEvent::CodexOp(_)) {
session_log::log_inbound_app_event(&event);
}
if let Err(e) = self.app_event_tx.send(event) {
tracing::error!("failed to send event: {e}");
}
Expand Down
2 changes: 0 additions & 2 deletions codex-rs/tui/src/bottom_pane/approval_modal_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,12 @@ impl<'a> BottomPaneView<'a> for ApprovalModalView<'a> {
mod tests {
use super::*;
use crate::app_event::AppEvent;
use std::path::PathBuf;
use std::sync::mpsc::channel;

fn make_exec_request() -> ApprovalRequest {
ApprovalRequest::Exec {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
cwd: PathBuf::from("/tmp"),
reason: None,
}
}
Expand Down
45 changes: 0 additions & 45 deletions codex-rs/tui/src/bottom_pane/live_ring_widget.rs

This file was deleted.

Loading
Loading