mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user