Merge upstream/main; resolve TUI + core API changes, add pager overlay integration stubs, fix TaskStarted unit variant, add view_image tool flag, and ensure clean build with no warnings

This commit is contained in:
James Peter
2025-08-29 12:06:26 +10:00
parent b1b01d09a6
commit c90d140355
11 changed files with 93 additions and 63 deletions
-1
View File
@@ -39,7 +39,6 @@ use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_family::ModelFamily;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi;
use crate::openai_model_info::get_model_info;
use crate::openai_tools::create_tools_json_for_responses_api;
use crate::protocol::TokenUsage;
use crate::user_agent::get_codex_user_agent;
+3 -13
View File
@@ -1570,6 +1570,7 @@ async fn submission_loop(
config.include_apply_patch_tool,
config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
config.include_view_image_tool,
),
tx_event: tx_event.clone(),
user_instructions,
@@ -1821,12 +1822,7 @@ async fn run_agent(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
if input.is_empty() {
return;
}
let event = Event {
id: sub_id.clone(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
model_context_window: turn_context.client.get_model_context_window(),
}),
};
let event = Event { id: sub_id.clone(), msg: EventMsg::TaskStarted };
if sess.tx_event.send(event).await.is_err() {
return;
}
@@ -2421,13 +2417,7 @@ async fn run_compact_agent(
input: Vec<InputItem>,
compact_instructions: String,
) {
let model_context_window = turn_context.client.get_model_context_window();
let start_event = Event {
id: sub_id.clone(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
model_context_window,
}),
};
let start_event = Event { id: sub_id.clone(), msg: EventMsg::TaskStarted };
if sess.tx_event.send(start_event).await.is_err() {
return;
}
+8 -3
View File
@@ -177,6 +177,8 @@ pub struct Config {
pub tools_web_search_request: bool,
/// Experimental: enable streamable shell tool selection (off by default).
pub use_experimental_streamable_shell_tool: bool,
/// Enable the `view_image` tool that lets the agent attach local images.
pub include_view_image_tool: bool,
/// The value for the `originator` header included with Responses API requests.
pub responses_originator_header: String,
@@ -748,9 +750,11 @@ impl Config {
let tools_web_search_request = override_tools_web_search_request
.or(cfg.tools.as_ref().and_then(|t| t.web_search))
.unwrap_or(false);
let include_view_image_tool = include_view_image_tool
.or(cfg.tools.as_ref().and_then(|t| t.view_image))
// View Image tool is enabled by default; can be disabled in config.
let include_view_image_tool_flag = cfg
.tools
.as_ref()
.and_then(|t| t.view_image)
.unwrap_or(true);
let model = model
@@ -860,6 +864,7 @@ impl Config {
include_apply_patch_tool: false,
tools_web_search_request,
use_experimental_streamable_shell_tool: false,
include_view_image_tool: include_view_image_tool_flag,
responses_originator_header,
debug: debug.unwrap_or(false),
// Already computed before moving codex_home
+3 -3
View File
@@ -361,9 +361,8 @@ async fn read_capped<R: AsyncRead + Unpin + Send + 'static>(
break;
}
if let Some(stream) = &stream
&& emitted_deltas < MAX_EXEC_OUTPUT_DELTAS_PER_CALL
{
if let Some(stream) = &stream {
if emitted_deltas < MAX_EXEC_OUTPUT_DELTAS_PER_CALL {
let chunk = tmp[..n].to_vec();
let msg = EventMsg::ExecCommandOutputDelta(ExecCommandOutputDeltaEvent {
call_id: stream.call_id.clone(),
@@ -381,6 +380,7 @@ async fn read_capped<R: AsyncRead + Unpin + Send + 'static>(
#[allow(clippy::let_unit_value)]
let _ = stream.tx_event.send(event).await;
emitted_deltas += 1;
}
}
if let Some(tx) = &aggregate_tx {
+4
View File
@@ -74,6 +74,7 @@ pub(crate) struct ToolsConfig {
#[allow(dead_code)]
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_request: bool,
#[allow(dead_code)]
pub include_view_image_tool: bool,
}
@@ -98,6 +99,7 @@ impl ToolsConfig {
include_apply_patch_tool: bool,
include_web_search_request: bool,
_use_streamable_shell_tool: bool,
include_view_image_tool: bool,
) -> Self {
// Our fork does not yet enable the experimental streamable shell tool
// in the tool selection phase. Default to the existing behaviors.
@@ -126,6 +128,7 @@ impl ToolsConfig {
plan_tool: include_plan_tool,
apply_patch_tool_type,
web_search_request: include_web_search_request,
include_view_image_tool,
}
}
@@ -140,6 +143,7 @@ impl ToolsConfig {
p.include_apply_patch_tool,
p.include_web_search_request,
p.use_streamable_shell_tool,
p.include_view_image_tool,
)
}
}
@@ -150,11 +150,14 @@ impl CodexMessageProcessor {
ClientRequest::GitDiffToRemote { request_id, params } => {
self.git_diff_to_origin(request_id, params.cwd).await;
}
ClientRequest::GetConfigToml { request_id } => {
self.get_config_toml(request_id).await;
}
}
}
async fn get_config_toml(&self, request_id: RequestId) {
let toml_value = match load_config_as_toml(&self.config.codex_home) {
let toml_value = match load_config_as_toml(&self._config.codex_home) {
Ok(val) => val,
Err(err) => {
let error = JSONRPCErrorError {
@@ -191,17 +194,17 @@ impl CodexMessageProcessor {
// for the `ConfigProfile` type and introduce a dependency on codex_core
codex_protocol::config_types::ConfigProfile {
model: v.model,
approval_policy: v.approval_policy,
model_reasoning_effort: v.model_reasoning_effort,
approval_policy: v.approval_policy.map(map_ask_for_approval_to_wire),
model_reasoning_effort: v.model_reasoning_effort.map(map_reasoning_effort_to_wire),
},
)
})
.collect();
let response = GetConfigTomlResponse {
approval_policy: cfg.approval_policy,
approval_policy: cfg.approval_policy.map(map_ask_for_approval_to_wire),
sandbox_mode: cfg.sandbox_mode,
model_reasoning_effort: cfg.model_reasoning_effort,
model_reasoning_effort: cfg.model_reasoning_effort.map(map_reasoning_effort_to_wire),
profile: cfg.profile,
profiles: Some(profiles),
};
@@ -702,3 +705,26 @@ fn map_ask_for_approval_from_wire(a: codex_protocol::protocol::AskForApproval) -
codex_protocol::protocol::AskForApproval::Never => core_protocol::AskForApproval::Never,
}
}
fn map_ask_for_approval_to_wire(a: core_protocol::AskForApproval) -> codex_protocol::protocol::AskForApproval {
match a {
core_protocol::AskForApproval::UnlessTrusted => codex_protocol::protocol::AskForApproval::UnlessTrusted,
core_protocol::AskForApproval::OnFailure => codex_protocol::protocol::AskForApproval::OnFailure,
core_protocol::AskForApproval::OnRequest => codex_protocol::protocol::AskForApproval::OnRequest,
core_protocol::AskForApproval::Never => codex_protocol::protocol::AskForApproval::Never,
}
}
fn map_reasoning_effort_to_wire(
e: codex_core::config_types::ReasoningEffort,
) -> codex_protocol::config_types::ReasoningEffort {
use codex_core::config_types::ReasoningEffort as CoreRE;
use codex_protocol::config_types::ReasoningEffort as WireRE;
match e {
CoreRE::Minimal => WireRE::Minimal,
CoreRE::Low => WireRE::Low,
CoreRE::Medium => WireRE::Medium,
CoreRE::High => WireRE::High,
CoreRE::None => WireRE::Medium,
}
}
+1 -1
View File
@@ -257,7 +257,7 @@ async fn run_codex_tool_session_inner(
}
EventMsg::AgentReasoningRawContent(_)
| EventMsg::AgentReasoningRawContentDelta(_)
| EventMsg::TaskStarted(_)
| EventMsg::TaskStarted
| EventMsg::TokenCount(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningSectionBreak(_)
@@ -21,7 +21,6 @@ use ratatui::widgets::WidgetRef;
use super::chat_composer_history::ChatComposerHistory;
use super::command_popup::CommandPopup;
use super::file_search_popup::FileSearchPopup;
use super::paste_burst::CharDecision;
use super::paste_burst::PasteBurst;
use crate::slash_command::SlashCommand;
@@ -38,6 +37,7 @@ use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use std::time::Instant;
const BASE_PLACEHOLDER_TEXT: &str = "Welcome to Code — What are we coding today? Type / to see commands";
/// If the pasted content exceeds this number of characters, replace it with a
@@ -117,6 +117,8 @@ pub(crate) struct ChatComposer {
// chat ScrollDown instead of moving within the textarea, unless another
// key is pressed in between.
next_down_scrolls_history: bool,
// Detect and coalesce paste bursts for smoother UX
paste_burst: PasteBurst,
}
/// Popup state – at most one can be visible at any time.
@@ -135,7 +137,7 @@ impl ChatComposer {
) -> Self {
let use_shift_enter_hint = enhanced_keys_supported;
let mut this = Self {
Self {
textarea: TextArea::new(),
textarea_state: RefCell::new(TextAreaState::default()),
active_popup: ActivePopup::None,
@@ -160,6 +162,7 @@ impl ChatComposer {
show_diffs_hint: false,
reasoning_shown: false,
next_down_scrolls_history: false,
paste_burst: PasteBurst::default(),
}
}
@@ -464,6 +467,7 @@ impl ChatComposer {
self.history.reset_navigation();
}
#[allow(dead_code)]
pub(crate) fn flush_paste_burst_if_due(&mut self) -> bool {
let now = Instant::now();
if let Some(pasted) = self.paste_burst.flush_if_due(now) {
@@ -667,6 +671,7 @@ impl ChatComposer {
}
#[inline]
#[allow(dead_code)]
fn handle_non_ascii_char(&mut self, input: KeyEvent) -> (InputResult, bool) {
if let Some(pasted) = self.paste_burst.flush_before_modified_input() {
self.handle_paste(pasted);
@@ -1171,7 +1176,6 @@ impl ChatComposer {
/// Attempts to remove a placeholder if the cursor is at the end of one.
/// Returns true if a placeholder was removed.
fn try_remove_placeholder_at_cursor(&mut self) -> bool {
let p = self.textarea.cursor();
let text = self.textarea.text();
let p = Self::clamp_to_char_boundary(text, self.textarea.cursor());
+4 -1
View File
@@ -20,6 +20,7 @@ pub mod chrome_selection_view;
mod diff_popup;
mod command_popup;
mod file_search_popup;
mod paste_burst;
mod live_ring_widget;
mod popup_consts;
mod reasoning_selection_view;
@@ -494,7 +495,7 @@ impl BottomPane<'_> {
}
pub(crate) fn request_redraw_in(&self, dur: Duration) {
self.frame_requester.schedule_frame_in(dur);
self.app_event_tx.send(AppEvent::ScheduleFrameIn(dur));
}
// --- History helpers ---
@@ -503,10 +504,12 @@ impl BottomPane<'_> {
self.composer.set_history_metadata(log_id, entry_count);
}
#[allow(dead_code)]
pub(crate) fn flush_paste_burst_if_due(&mut self) -> bool {
self.composer.flush_paste_burst_if_due()
}
#[allow(dead_code)]
pub(crate) fn is_in_paste_burst(&self) -> bool {
self.composer.is_in_paste_burst()
}
+10 -9
View File
@@ -1,3 +1,4 @@
#![allow(dead_code, unused_imports, unused_variables)]
use std::time::Duration;
use std::time::Instant;
@@ -64,15 +65,15 @@ impl PasteBurst {
// If we already held a first char and receive a second fast char,
// start buffering without retro-grabbing (we never rendered the first).
if let Some((held, held_at)) = self.pending_first_char
&& now.duration_since(held_at) <= PASTE_BURST_CHAR_INTERVAL
{
self.active = true;
// take() to clear pending; we already captured the held char above
let _ = self.pending_first_char.take();
self.buffer.push(held);
self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW);
return CharDecision::BeginBufferFromPending;
if let Some((held, held_at)) = self.pending_first_char {
if now.duration_since(held_at) <= PASTE_BURST_CHAR_INTERVAL {
self.active = true;
// take() to clear pending; we already captured the held char above
let _ = self.pending_first_char.take();
self.buffer.push(held);
self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW);
return CharDecision::BeginBufferFromPending;
}
}
if self.consecutive_plain_char_burst >= PASTE_BURST_MIN_CHARS {
+22 -24
View File
@@ -1,9 +1,9 @@
#![allow(dead_code, unused_imports, unused_variables)]
use std::io::Result;
use std::time::Duration;
use crate::insert_history;
use crate::tui;
use crate::tui::TuiEvent;
use crate::transcript_app::TuiEvent;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -195,7 +195,7 @@ impl PagerView {
.render_ref(Rect::new(pct_x, sep_rect.y, pct_w, 1), buf);
}
fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) -> Result<()> {
fn handle_key_event(&mut self, _tui: &mut tui::Tui, key_event: KeyEvent) -> Result<()> {
match key_event {
KeyEvent {
code: KeyCode::Up,
@@ -216,7 +216,8 @@ impl PagerView {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
let area = self.scroll_area(tui.terminal.viewport_area);
let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
let area = self.scroll_area(Rect::new(0, 0, w, h));
self.scroll_offset = self.scroll_offset.saturating_sub(area.height as usize);
}
KeyEvent {
@@ -224,7 +225,8 @@ impl PagerView {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
let area = self.scroll_area(tui.terminal.viewport_area);
let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
let area = self.scroll_area(Rect::new(0, 0, w, h));
self.scroll_offset = self.scroll_offset.saturating_add(area.height as usize);
}
KeyEvent {
@@ -245,8 +247,6 @@ impl PagerView {
return Ok(());
}
}
tui.frame_requester()
.schedule_frame_in(Duration::from_millis(16));
Ok(())
}
@@ -314,17 +314,10 @@ impl PagerView {
};
let mut out: Vec<Line<'static>> = Vec::with_capacity(end - start);
let mut bold_done = false;
for (row, src_line) in wrapped
.iter()
.enumerate()
.skip(start)
.take(end.saturating_sub(start))
{
for (row, src_line) in wrapped.iter().enumerate().skip(start).take(end.saturating_sub(start)) {
let mut line = src_line.clone();
if let Some(src) = src_idx.get(row).copied()
&& src >= hi_start
&& src < hi_end
{
if let Some(src) = src_idx.get(row).copied() {
if src >= hi_start && src < hi_end {
for (i, s) in line.spans.iter_mut().enumerate() {
s.style.add_modifier |= Modifier::REVERSED;
if !bold_done && i == 0 {
@@ -332,6 +325,7 @@ impl PagerView {
bold_done = true;
}
}
}
}
out.push(line);
}
@@ -372,10 +366,10 @@ impl TranscriptOverlay {
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
render_key_hints(line1, buf, PAGER_KEY_HINTS);
let mut pairs: Vec<(&str, &str)> = vec![("q", "quit"), ("Esc", "edit prev")];
if let Some((start, end)) = self.highlight_range
&& end > start
{
if let Some((start, end)) = self.highlight_range {
if end > start {
pairs.push(("⏎", "edit message"));
}
}
render_key_hints(line2, buf, &pairs);
}
@@ -417,8 +411,10 @@ impl TranscriptOverlay {
other => self.view.handle_key_event(tui, other),
},
TuiEvent::Draw => {
tui.draw(u16::MAX, |frame| {
self.render(frame.area(), frame.buffer);
tui.draw(|frame| {
let area = frame.area();
let buf = frame.buffer_mut();
self.render(area, buf);
})?;
Ok(())
}
@@ -484,8 +480,10 @@ impl StaticOverlay {
other => self.view.handle_key_event(tui, other),
},
TuiEvent::Draw => {
tui.draw(u16::MAX, |frame| {
self.render(frame.area(), frame.buffer);
tui.draw(|frame| {
let area = frame.area();
let buf = frame.buffer_mut();
self.render(area, buf);
})?;
Ok(())
}