Merge branch 'main' into fix-readme-agents-md

This commit is contained in:
pap-openai
2025-08-01 01:15:49 +01:00
committed by GitHub
6 changed files with 201 additions and 201 deletions
+7 -1
View File
@@ -192,7 +192,7 @@ impl ModelClient {
loop {
attempt += 1;
let req_builder = self
let mut req_builder = self
.client
.post(format!("{base_url}/responses"))
.header("OpenAI-Beta", "responses=experimental")
@@ -201,6 +201,12 @@ impl ModelClient {
.header(reqwest::header::ACCEPT, "text/event-stream")
.json(&payload);
if auth.mode == AuthMode::ChatGPT {
if let Some(account_id) = auth.get_account_id().await {
req_builder = req_builder.header("chatgpt-account-id", account_id);
}
}
let req_builder = self.provider.apply_http_headers(req_builder);
let res = req_builder.send().await;
+3 -1
View File
@@ -237,6 +237,7 @@ async fn chatgpt_auth_sends_correct_request() {
let request_session_id = request.headers.get("session_id").unwrap();
let request_originator = request.headers.get("originator").unwrap();
let request_authorization = request.headers.get("authorization").unwrap();
let request_chatgpt_account_id = request.headers.get("chatgpt-account-id").unwrap();
let request_body = request.body_json::<serde_json::Value>().unwrap();
assert!(current_session_id.is_some());
@@ -249,6 +250,7 @@ async fn chatgpt_auth_sends_correct_request() {
request_authorization.to_str().unwrap(),
"Bearer Access Token"
);
assert_eq!(request_chatgpt_account_id.to_str().unwrap(), "account_id");
assert!(!request_body["store"].as_bool().unwrap());
assert!(request_body["stream"].as_bool().unwrap());
assert_eq!(
@@ -332,7 +334,7 @@ fn auth_from_token(id_token: String) -> CodexAuth {
id_token,
access_token: "Access Token".to_string(),
refresh_token: "test".to_string(),
account_id: None,
account_id: Some("account_id".to_string()),
}),
last_refresh: Some(Utc::now()),
}),
+11
View File
@@ -123,6 +123,17 @@ impl CodexAuth {
}
}
}
pub async fn get_account_id(&self) -> Option<String> {
match self.mode {
AuthMode::ApiKey => None,
AuthMode::ChatGPT => {
let token_data = self.get_token_data().await.ok()?;
token_data.account_id.clone()
}
}
}
}
// Loads the available auth information from the auth.json or OPENAI_API_KEY environment variable.
+56 -12
View File
@@ -10,13 +10,14 @@ use crate::tui;
use codex_core::config::Config;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecApprovalRequestEvent;
use color_eyre::eyre::Result;
use crossterm::SynchronizedUpdate;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use ratatui::layout::Offset;
use ratatui::prelude::Backend;
use ratatui::text::Line;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -55,6 +56,8 @@ pub(crate) struct App<'a> {
/// True when a redraw has been scheduled but not yet executed.
pending_redraw: Arc<AtomicBool>,
pending_history_lines: Vec<Line<'static>>,
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
/// after dismissing the Git-repo warning.
chat_args: Option<ChatWidgetArgs>,
@@ -96,9 +99,7 @@ impl App<'_> {
if let Ok(event) = crossterm::event::read() {
match event {
crossterm::event::Event::Key(key_event) => {
if key_event.kind == crossterm::event::KeyEventKind::Press {
app_event_tx.send(AppEvent::KeyEvent(key_event));
}
app_event_tx.send(AppEvent::KeyEvent(key_event));
}
crossterm::event::Event::Resize(_, _) => {
app_event_tx.send(AppEvent::RequestRedraw);
@@ -154,6 +155,7 @@ impl App<'_> {
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
Self {
app_event_tx,
pending_history_lines: Vec::new(),
app_event_rx,
app_state,
config,
@@ -199,7 +201,7 @@ impl App<'_> {
while let Ok(event) = self.app_event_rx.recv() {
match event {
AppEvent::InsertHistory(lines) => {
crate::insert_history::insert_history_lines(terminal, lines);
self.pending_history_lines.extend(lines);
self.app_event_tx.send(AppEvent::RequestRedraw);
}
AppEvent::RequestRedraw => {
@@ -213,6 +215,7 @@ impl App<'_> {
KeyEvent {
code: KeyCode::Char('c'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
match &mut self.app_state {
@@ -227,6 +230,7 @@ impl App<'_> {
KeyEvent {
code: KeyCode::Char('d'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
match &mut self.app_state {
@@ -245,9 +249,15 @@ impl App<'_> {
}
}
}
_ => {
KeyEvent {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
self.dispatch_key_event(key_event);
}
_ => {
// Ignore Release key events for now.
}
};
}
AppEvent::Paste(text) => {
@@ -304,14 +314,41 @@ impl App<'_> {
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use std::collections::HashMap;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::FileChange;
self.app_event_tx.send(AppEvent::CodexEvent(Event {
id: "1".to_string(),
msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id: "1".to_string(),
command: vec!["git".into(), "apply".into()],
cwd: self.config.cwd.clone(),
reason: Some("test".to_string()),
}),
// msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
// call_id: "1".to_string(),
// command: vec!["git".into(), "apply".into()],
// cwd: self.config.cwd.clone(),
// reason: Some("test".to_string()),
// }),
msg: EventMsg::ApplyPatchApprovalRequest(
ApplyPatchApprovalRequestEvent {
call_id: "1".to_string(),
changes: HashMap::from([
(
PathBuf::from("/tmp/test.txt"),
FileChange::Add {
content: "test".to_string(),
},
),
(
PathBuf::from("/tmp/test2.txt"),
FileChange::Update {
unified_diff: "+test\n-test2".to_string(),
move_path: None,
},
),
]),
reason: None,
grant_root: Some(PathBuf::from("/tmp")),
},
),
}));
}
},
@@ -376,6 +413,13 @@ impl App<'_> {
terminal.clear()?;
terminal.set_viewport_area(area);
}
if !self.pending_history_lines.is_empty() {
crate::insert_history::insert_history_lines(
terminal,
self.pending_history_lines.clone(),
);
self.pending_history_lines.clear();
}
match &mut self.app_state {
AppState::Chat { widget } => {
terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;
+4 -1
View File
@@ -25,6 +25,7 @@ use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TokenUsage;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
@@ -157,7 +158,9 @@ impl ChatWidget<'_> {
}
pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
self.bottom_pane.clear_ctrl_c_quit_hint();
if key_event.kind == KeyEventKind::Press {
self.bottom_pane.clear_ctrl_c_quit_hint();
}
match self.bottom_pane.handle_key_event(key_event) {
InputResult::Submitted(text) => {
+120 -186
View File
@@ -7,23 +7,24 @@
//! driven workflow – a fully‑fledged visual match is not required.
use std::path::PathBuf;
use std::sync::LazyLock;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewDecision;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::*;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::List;
use ratatui::widgets::Block;
use ratatui::widgets::BorderType;
use ratatui::widgets::Borders;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
@@ -47,68 +48,62 @@ pub(crate) enum ApprovalRequest {
/// Options displayed in the *select* mode.
struct SelectOption {
label: &'static str,
decision: Option<ReviewDecision>,
/// `true` when this option switches the widget to *input* mode.
enters_input_mode: bool,
label: Line<'static>,
description: &'static str,
key: KeyCode,
decision: ReviewDecision,
}
// keep in same order as in the TS implementation
const SELECT_OPTIONS: &[SelectOption] = &[
SelectOption {
label: "Yes (y)",
decision: Some(ReviewDecision::Approved),
static COMMAND_SELECT_OPTIONS: LazyLock<Vec<SelectOption>> = LazyLock::new(|| {
vec![
SelectOption {
label: Line::from(vec!["Y".underlined(), "es".into()]),
description: "Approve and run the command",
key: KeyCode::Char('y'),
decision: ReviewDecision::Approved,
},
SelectOption {
label: Line::from(vec!["A".underlined(), "lways".into()]),
description: "Approve the command for the remainder of this session",
key: KeyCode::Char('a'),
decision: ReviewDecision::ApprovedForSession,
},
SelectOption {
label: Line::from(vec!["N".underlined(), "o".into()]),
description: "Do not run the command",
key: KeyCode::Char('n'),
decision: ReviewDecision::Denied,
},
]
});
enters_input_mode: false,
},
SelectOption {
label: "Yes, always approve this exact command for this session (a)",
decision: Some(ReviewDecision::ApprovedForSession),
enters_input_mode: false,
},
SelectOption {
label: "Edit or give feedback (e)",
decision: None,
enters_input_mode: true,
},
SelectOption {
label: "No, and keep going (n)",
decision: Some(ReviewDecision::Denied),
enters_input_mode: false,
},
SelectOption {
label: "No, and stop for now (esc)",
decision: Some(ReviewDecision::Abort),
enters_input_mode: false,
},
];
/// Internal mode the widget is in – mirrors the TypeScript component.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Select,
Input,
}
static PATCH_SELECT_OPTIONS: LazyLock<Vec<SelectOption>> = LazyLock::new(|| {
vec![
SelectOption {
label: Line::from(vec!["Y".underlined(), "es".into()]),
description: "Approve and apply the changes",
key: KeyCode::Char('y'),
decision: ReviewDecision::Approved,
},
SelectOption {
label: Line::from(vec!["N".underlined(), "o".into()]),
description: "Do not apply the changes",
key: KeyCode::Char('n'),
decision: ReviewDecision::Denied,
},
]
});
/// A modal prompting the user to approve or deny the pending request.
pub(crate) struct UserApprovalWidget<'a> {
approval_request: ApprovalRequest,
app_event_tx: AppEventSender,
confirmation_prompt: Paragraph<'a>,
select_options: &'a Vec<SelectOption>,
/// Currently selected index in *select* mode.
selected_option: usize,
/// State for the optional input widget.
input: Input,
/// Current mode.
mode: Mode,
/// Set to `true` once a decision has been sent – the parent view can then
/// remove this widget from its queue.
done: bool,
@@ -116,7 +111,6 @@ pub(crate) struct UserApprovalWidget<'a> {
impl UserApprovalWidget<'_> {
pub(crate) fn new(approval_request: ApprovalRequest, app_event_tx: AppEventSender) -> Self {
let input = Input::default();
let confirmation_prompt = match &approval_request {
ApprovalRequest::Exec {
command,
@@ -132,25 +126,20 @@ impl UserApprovalWidget<'_> {
None => cwd.display().to_string(),
};
let mut contents: Vec<Line> = vec![
Line::from(vec![
Span::from(cwd_str).dim(),
Span::from("$"),
Span::from(format!(" {cmd}")),
]),
Line::from(vec!["codex".bold().magenta(), " wants to run:".into()]),
Line::from(vec![cwd_str.dim(), "$".into(), format!(" {cmd}").into()]),
Line::from(""),
];
if let Some(reason) = reason {
contents.push(Line::from(reason.clone().italic()));
contents.push(Line::from(""));
}
contents.extend(vec![Line::from("Allow command?"), Line::from("")]);
Paragraph::new(contents).wrap(Wrap { trim: false })
}
ApprovalRequest::ApplyPatch {
reason, grant_root, ..
} => {
let mut contents: Vec<Line> =
vec![Line::from("Apply patch".bold()), Line::from("")];
let mut contents: Vec<Line> = vec![];
if let Some(r) = reason {
contents.push(Line::from(r.clone().italic()));
@@ -165,20 +154,19 @@ impl UserApprovalWidget<'_> {
contents.push(Line::from(""));
}
contents.push(Line::from("Allow changes?"));
contents.push(Line::from(""));
Paragraph::new(contents)
Paragraph::new(contents).wrap(Wrap { trim: false })
}
};
Self {
select_options: match &approval_request {
ApprovalRequest::Exec { .. } => &COMMAND_SELECT_OPTIONS,
ApprovalRequest::ApplyPatch { .. } => &PATCH_SELECT_OPTIONS,
},
approval_request,
app_event_tx,
confirmation_prompt,
selected_option: 0,
input,
mode: Mode::Select,
done: false,
}
}
@@ -194,9 +182,8 @@ impl UserApprovalWidget<'_> {
/// captures input while visible, we don’t need to report whether the event
/// was consumed—callers can assume it always is.
pub(crate) fn handle_key_event(&mut self, key: KeyEvent) {
match self.mode {
Mode::Select => self.handle_select_key(key),
Mode::Input => self.handle_input_key(key),
if key.kind == KeyEventKind::Press {
self.handle_select_key(key);
}
}
@@ -208,58 +195,24 @@ impl UserApprovalWidget<'_> {
fn handle_select_key(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Up => {
if self.selected_option == 0 {
self.selected_option = SELECT_OPTIONS.len() - 1;
} else {
self.selected_option -= 1;
}
KeyCode::Left => {
self.selected_option = (self.selected_option + self.select_options.len() - 1)
% self.select_options.len();
}
KeyCode::Down => {
self.selected_option = (self.selected_option + 1) % SELECT_OPTIONS.len();
}
KeyCode::Char('y') => {
self.send_decision(ReviewDecision::Approved);
}
KeyCode::Char('a') => {
self.send_decision(ReviewDecision::ApprovedForSession);
}
KeyCode::Char('n') => {
self.send_decision(ReviewDecision::Denied);
}
KeyCode::Char('e') => {
self.mode = Mode::Input;
KeyCode::Right => {
self.selected_option = (self.selected_option + 1) % self.select_options.len();
}
KeyCode::Enter => {
let opt = &SELECT_OPTIONS[self.selected_option];
if opt.enters_input_mode {
self.mode = Mode::Input;
} else if let Some(decision) = opt.decision {
self.send_decision(decision);
}
let opt = &self.select_options[self.selected_option];
self.send_decision(opt.decision);
}
KeyCode::Esc => {
self.send_decision(ReviewDecision::Abort);
}
_ => {}
}
}
fn handle_input_key(&mut self, key_event: KeyEvent) {
// Handle special keys first.
match key_event.code {
KeyCode::Enter => {
let feedback = self.input.value().to_string();
self.send_decision_with_feedback(ReviewDecision::Denied, feedback);
}
KeyCode::Esc => {
// Cancel input – treat as deny without feedback.
self.send_decision(ReviewDecision::Denied);
}
_ => {
// Feed into input widget for normal editing.
let ct_event = crossterm::event::Event::Key(key_event);
self.input.handle_event(&ct_event);
other => {
if let Some(opt) = self.select_options.iter().find(|opt| opt.key == other) {
self.send_decision(opt.decision);
}
}
}
}
@@ -312,87 +265,68 @@ impl UserApprovalWidget<'_> {
}
pub(crate) fn desired_height(&self, width: u16) -> u16 {
self.get_confirmation_prompt_height(width - 2) + SELECT_OPTIONS.len() as u16 + 2
self.get_confirmation_prompt_height(width) + self.select_options.len() as u16
}
}
const PLAIN: Style = Style::new();
const BLUE_FG: Style = Style::new().fg(Color::LightCyan);
impl WidgetRef for &UserApprovalWidget<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
// Take the area, wrap it in a block with a border, and divide up the
// remaining area into two chunks: one for the confirmation prompt and
// one for the response.
let inner = area.inner(Margin::new(0, 2));
// Determine how many rows we can allocate for the static confirmation
// prompt while *always* keeping enough space for the interactive
// response area (select list or input field). When the full prompt
// would exceed the available height we truncate it so the response
// options never get pushed out of view. This keeps the approval modal
// usable even when the overall bottom viewport is small.
// Full height of the prompt (may be larger than the available area).
let full_prompt_height = self.get_confirmation_prompt_height(inner.width);
// Minimum rows that must remain for the interactive section.
let min_response_rows = match self.mode {
Mode::Select => SELECT_OPTIONS.len() as u16,
// In input mode we need exactly two rows: one for the guidance
// prompt and one for the single-line input field.
Mode::Input => 2,
};
// Clamp prompt height so confirmation + response never exceed the
// available space. `saturating_sub` avoids underflow when the area is
// too small even for the minimal layout – in this unlikely case we
// fall back to zero-height prompt so at least the options are
// visible.
let prompt_height = full_prompt_height.min(inner.height.saturating_sub(min_response_rows));
let chunks = Layout::default()
let prompt_height = self.get_confirmation_prompt_height(area.width);
let [prompt_chunk, response_chunk] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(prompt_height), Constraint::Min(0)])
.split(inner);
let prompt_chunk = chunks[0];
let response_chunk = chunks[1];
.areas(area);
// Build the inner lines based on the mode. Collect them into a List of
// non-wrapping lines rather than a Paragraph for predictable layout.
let lines = match self.mode {
Mode::Select => SELECT_OPTIONS
.iter()
.enumerate()
.map(|(idx, opt)| {
let (prefix, style) = if idx == self.selected_option {
("▶", BLUE_FG)
} else {
(" ", PLAIN)
};
Line::styled(format!(" {prefix} {}", opt.label), style)
})
.collect(),
Mode::Input => {
vec![
Line::from("Give the model feedback on this command:"),
Line::from(self.input.value()),
]
}
let lines: Vec<Line> = self
.select_options
.iter()
.enumerate()
.map(|(idx, opt)| {
let style = if idx == self.selected_option {
Style::new().bg(Color::Cyan).fg(Color::Black)
} else {
Style::new().bg(Color::DarkGray)
};
opt.label.clone().alignment(Alignment::Center).style(style)
})
.collect();
let [title_area, button_area, description_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.areas(response_chunk.inner(Margin::new(1, 0)));
let title = match &self.approval_request {
ApprovalRequest::Exec { .. } => "Allow command?",
ApprovalRequest::ApplyPatch { .. } => "Apply changes?",
};
let border = ("◢◤")
.repeat((area.width / 2).into())
.fg(Color::LightYellow);
border.render_ref(area, buf);
Paragraph::new(" Execution Request ".bold().black().on_light_yellow())
.alignment(Alignment::Center)
.render_ref(area, buf);
Line::from(title).render(title_area, buf);
self.confirmation_prompt.clone().render(prompt_chunk, buf);
List::new(lines).render_ref(response_chunk, buf);
let areas = Layout::horizontal(
lines
.iter()
.map(|l| Constraint::Length(l.width() as u16 + 2)),
)
.spacing(1)
.split(button_area);
for (idx, area) in areas.iter().enumerate() {
let line = &lines[idx];
line.render(*area, buf);
}
border.render_ref(Rect::new(0, area.y + area.height - 1, area.width, 1), buf);
Line::from(self.select_options[self.selected_option].description)
.style(Style::new().italic().fg(Color::DarkGray))
.render(description_area.inner(Margin::new(1, 0)), buf);
Block::bordered()
.border_type(BorderType::QuadrantOutside)
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT)
.render_ref(
Rect::new(0, response_chunk.y, 1, response_chunk.height),
buf,
);
}
}