Merge upstream/main with -X ours; adapt to protocol and TUI changes

- core: replace StreamError with Error events; wrap EnvironmentContext args; fix let chains
- exec: drop StreamError handling (use Error)
- mcp-server: stub unsupported auth methods; remove StreamError match
- tui: wire transcript overlay types; add missing imports; initialize new fields
- composer: add Command variant to InputResult
- chatwidget: remove legacy dispatch_command; handle Command result
- lib: expose transcript_app module
- transcript_app: update to current TUI API (draw, size)
This commit is contained in:
James Peter
2025-08-22 23:38:20 +10:00
parent b1833fb4eb
commit 0f1dfd8280
10 changed files with 54 additions and 117 deletions
+5 -6
View File
@@ -310,7 +310,6 @@ use crate::protocol::PatchApplyEndEvent;
use crate::protocol::ReviewDecision;
use crate::protocol::SandboxPolicy;
use crate::protocol::SessionConfiguredEvent;
use crate::protocol::StreamErrorEvent;
use crate::protocol::Submission;
use crate::protocol::TaskCompleteEvent;
use crate::protocol::TurnDiffEvent;
@@ -910,7 +909,7 @@ impl Session {
async fn notify_stream_error(&self, sub_id: &str, message: impl Into<String>) {
let event = Event {
id: sub_id.to_string(),
msg: EventMsg::StreamError(StreamErrorEvent {
msg: EventMsg::Error(ErrorEvent {
message: message.into(),
}),
};
@@ -1856,10 +1855,10 @@ async fn run_turn(
tools: tools.clone(),
base_instructions_override: sess.base_instructions.clone(),
environment_context: Some(EnvironmentContext::new(
sess.cwd.clone(),
sess.approval_policy,
sess.sandbox_policy.clone(),
sess.user_shell.clone(),
Some(sess.cwd.clone()),
Some(sess.approval_policy),
Some(sess.sandbox_policy.clone()),
Some(sess.user_shell.clone()),
)),
status_items, // Include status items with this request
};
+4 -4
View File
@@ -98,10 +98,10 @@ impl EnvironmentContext {
network_access
));
}
if let Some(shell) = self.shell
&& let Some(shell_name) = shell.name()
{
lines.push(format!(" <shell>{}</shell>", shell_name));
if let Some(shell) = self.shell {
if let Some(shell_name) = shell.name() {
lines.push(format!(" <shell>{}</shell>", shell_name));
}
}
lines.push(ENVIRONMENT_CONTEXT_END.to_string());
lines.join("\n")
@@ -20,7 +20,6 @@ use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::PatchApplyEndEvent;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
use codex_protocol::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
@@ -175,9 +174,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
ts_println!(self, "{}", message.style(self.dimmed));
}
EventMsg::StreamError(StreamErrorEvent { message }) => {
ts_println!(self, "{}", message.style(self.dimmed));
}
// Stream errors are surfaced as Error/Background events in core
EventMsg::TaskStarted => {
// Ignore.
}
@@ -148,10 +148,22 @@ impl CodexMessageProcessor {
self.outgoing.send_error(request_id, error).await;
}
ClientRequest::LogoutChatGpt { request_id } => {
self.logout_chatgpt(request_id).await;
// Not supported by this server implementation
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: "logout is not supported by this server".to_string(),
data: None,
};
self.outgoing.send_error(request_id, error).await;
}
ClientRequest::GetAuthStatus { request_id } => {
self.get_auth_status(request_id).await;
// Not supported by this server implementation
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: "auth status is not supported by this server".to_string(),
data: None,
};
self.outgoing.send_error(request_id, error).await;
}
ClientRequest::GitDiffToRemote { request_id, params } => {
self.git_diff_to_origin(request_id, params.cwd).await;
@@ -268,7 +268,6 @@ async fn run_codex_tool_session_inner(
| EventMsg::ExecCommandOutputDelta(_)
| EventMsg::ExecCommandEnd(_)
| EventMsg::BackgroundEvent(_)
| EventMsg::StreamError(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyEnd(_)
| EventMsg::TurnDiff(_)
+6
View File
@@ -8,6 +8,7 @@ use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::OnboardingScreen;
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
use crate::slash_command::SlashCommand;
use crate::transcript_app::TranscriptApp;
use crate::tui;
use crate::tui::TerminalInfo;
use codex_core::ConversationManager;
@@ -24,6 +25,8 @@ use crossterm::terminal::EnterAlternateScreen;
use crossterm::terminal::LeaveAlternateScreen;
use crossterm::terminal::supports_keyboard_enhancement;
use std::path::PathBuf;
use ratatui::prelude::Rect;
use ratatui::text::Line;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
@@ -209,6 +212,9 @@ impl App<'_> {
config,
file_search,
pending_redraw,
transcript_overlay: None,
deferred_history_lines: Vec::new(),
transcript_saved_viewport: None,
enhanced_keys_supported,
_debug: debug,
commit_anim_running: Arc::new(AtomicBool::new(false)),
@@ -42,6 +42,7 @@ const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000;
/// Result returned when the user interacts with the text area.
pub enum InputResult {
Submitted(String),
Command(SlashCommand),
ScrollUp,
ScrollDown,
None,
+7 -95
View File
@@ -58,6 +58,8 @@ use std::cell::RefCell;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::mpsc::unbounded_channel;
use tracing::info;
use crate::slash_command::SlashCommand;
use crate::get_git_diff::get_git_diff;
// use image::GenericImageView;
use crate::app_event::AppEvent;
@@ -1453,6 +1455,10 @@ impl ChatWidget<'_> {
let user_message = self.parse_message_with_images(text);
self.submit_user_message(user_message);
}
InputResult::Command(_cmd) => {
// Command was dispatched at the App layer; request redraw.
self.app_event_tx.send(AppEvent::RequestRedraw);
}
InputResult::ScrollUp => {
// If already at the very top, try navigating command history instead
if self.scroll_offset >= self.last_max_scroll.get() {
@@ -1523,101 +1529,7 @@ impl ChatWidget<'_> {
}
}
fn dispatch_command(&mut self, cmd: SlashCommand) {
match cmd {
SlashCommand::New => {
self.app_event_tx.send(AppEvent::NewSession);
}
SlashCommand::Init => {
// Guard: do not run if a task is active.
const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md");
self.submit_text_message(INIT_PROMPT.to_string());
}
SlashCommand::Compact => {
self.clear_token_usage();
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
}
SlashCommand::Model => {
self.open_model_popup();
}
SlashCommand::Approvals => {
self.open_approvals_popup();
}
SlashCommand::Quit => {
self.app_event_tx.send(AppEvent::ExitRequest);
}
SlashCommand::Logout => {
if let Err(e) = codex_login::logout(&self.config.codex_home) {
tracing::error!("failed to logout: {e}");
}
self.app_event_tx.send(AppEvent::ExitRequest);
}
SlashCommand::Diff => {
self.add_diff_in_progress();
let tx = self.app_event_tx.clone();
tokio::spawn(async move {
let text = match get_git_diff().await {
Ok((is_git_repo, diff_text)) => {
if is_git_repo {
diff_text
} else {
"`/diff` — _not inside a git repository_".to_string()
}
}
Err(e) => format!("Failed to compute diff: {e}"),
};
tx.send(AppEvent::DiffResult(text));
});
}
SlashCommand::Mention => {
self.insert_str("@");
}
SlashCommand::Status => {
self.add_status_output();
}
SlashCommand::Mcp => {
self.add_mcp_output();
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use codex_core::protocol::EventMsg;
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::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")),
}),
}));
}
}
}
// dispatch_command() removed — command routing is handled at the App layer via AppEvent::DispatchCommand
pub(crate) fn handle_paste(&mut self, text: String) {
// Check if the pasted text is a file path to an image
+1
View File
@@ -58,6 +58,7 @@ mod theme;
mod tui;
mod user_approval_widget;
mod height_manager;
mod transcript_app;
// Internal vt100-based replay tests live as a separate source file to keep them
// close to the widget code. Include them in unit tests.
+15 -5
View File
@@ -2,7 +2,6 @@ use std::io::Result;
use crate::insert_history;
use crate::tui;
use crate::tui::TuiEvent;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -36,8 +35,10 @@ impl TranscriptApp {
match event {
TuiEvent::Key(key_event) => self.handle_key_event(tui, key_event),
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);
})?;
}
_ => {}
@@ -89,7 +90,8 @@ impl TranscriptApp {
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 {
@@ -97,7 +99,8 @@ impl TranscriptApp {
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 {
@@ -207,3 +210,10 @@ impl TranscriptApp {
.render_ref(hints_rect, buf);
}
}
/// Minimal local event enum for transcript overlay rendering
pub(crate) enum TuiEvent {
Key(KeyEvent),
Draw,
Other,
}