mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
Merge cc0c30a50d into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -82,10 +82,34 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
const ILLEGAL_ENV_VAR_PREFIX: &str = "CONST_";
|
||||
|
||||
/// Load env vars from ~/.codex/.env and `$(pwd)/.env`.
|
||||
///
|
||||
/// Security: Do not allow `.env` files to create or modify any variables
|
||||
/// with names starting with `CODEX_`.
|
||||
fn load_dotenv() {
|
||||
if let Ok(codex_home) = codex_core::config::find_codex_home() {
|
||||
dotenvy::from_path(codex_home.join(".env")).ok();
|
||||
// Helper to set vars from a dotenvy iterator while filtering out `CODEX_` keys.
|
||||
fn set_filtered<I>(iter: I)
|
||||
where
|
||||
I: IntoIterator<Item = Result<(String, String), dotenvy::Error>>,
|
||||
{
|
||||
for (key, value) in iter.into_iter().flatten() {
|
||||
if !key.starts_with(ILLEGAL_ENV_VAR_PREFIX) {
|
||||
// It is safe to call set_var() because our process is
|
||||
// single-threaded at this point in its execution.
|
||||
unsafe { std::env::set_var(&key, &value) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(codex_home) = codex_core::config::find_codex_home() {
|
||||
if let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env")) {
|
||||
set_filtered(iter);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(iter) = dotenvy::dotenv_iter() {
|
||||
set_filtered(iter);
|
||||
}
|
||||
dotenvy::dotenv().ok();
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ pub struct RemoveConversationListenerParams {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
pub enum InputItem {
|
||||
Text {
|
||||
text: String,
|
||||
|
||||
@@ -183,12 +183,6 @@ impl App<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone of the internal event sender so external tasks (e.g. log bridge)
|
||||
/// can inject `AppEvent`s.
|
||||
pub fn event_sender(&self) -> AppEventSender {
|
||||
self.app_event_tx.clone()
|
||||
}
|
||||
|
||||
/// Schedule a redraw if one is not already pending.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn schedule_redraw(&self) {
|
||||
@@ -325,10 +319,6 @@ impl App<'_> {
|
||||
AppState::Chat { widget } => widget.submit_op(op),
|
||||
AppState::Onboarding { .. } => {}
|
||||
},
|
||||
AppEvent::LatestLog(line) => match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.update_latest_log(line),
|
||||
AppState::Onboarding { .. } => {}
|
||||
},
|
||||
AppEvent::DispatchCommand(command) => match command {
|
||||
SlashCommand::New => {
|
||||
// User accepted – switch to chat view.
|
||||
|
||||
@@ -29,9 +29,6 @@ pub(crate) enum AppEvent {
|
||||
/// bubbling channels through layers of widgets.
|
||||
CodexOp(codex_core::protocol::Op),
|
||||
|
||||
/// Latest formatted log line emitted by `tracing`.
|
||||
LatestLog(String),
|
||||
|
||||
/// Dispatch a recognized slash command from the UI (composer) to the app
|
||||
/// layer so it can be handled centrally.
|
||||
DispatchCommand(SlashCommand),
|
||||
|
||||
@@ -6,12 +6,6 @@ use ratatui::layout::Rect;
|
||||
use super::BottomPane;
|
||||
use super::CancellationEvent;
|
||||
|
||||
/// Type to use for a method that may require a redraw of the UI.
|
||||
pub(crate) enum ConditionalUpdate {
|
||||
NeedsRedraw,
|
||||
NoRedraw,
|
||||
}
|
||||
|
||||
/// Trait implemented by every view that can be shown in the bottom pane.
|
||||
pub(crate) trait BottomPaneView<'a> {
|
||||
/// Handle a key event while the view is active. A redraw is always
|
||||
@@ -34,11 +28,6 @@ pub(crate) trait BottomPaneView<'a> {
|
||||
/// Render the view: this will be displayed in place of the composer.
|
||||
fn render(&self, area: Rect, buf: &mut Buffer);
|
||||
|
||||
/// Update the status indicator text.
|
||||
fn update_status_text(&mut self, _text: String) -> ConditionalUpdate {
|
||||
ConditionalUpdate::NoRedraw
|
||||
}
|
||||
|
||||
/// Called when task completes to check if the view should be hidden.
|
||||
fn should_hide_when_task_is_done(&mut self) -> bool {
|
||||
false
|
||||
|
||||
@@ -32,7 +32,6 @@ pub(crate) enum CancellationEvent {
|
||||
pub(crate) use chat_composer::ChatComposer;
|
||||
pub(crate) use chat_composer::InputResult;
|
||||
|
||||
use crate::status_indicator_widget::StatusIndicatorWidget;
|
||||
use approval_modal_view::ApprovalModalView;
|
||||
use status_indicator_view::StatusIndicatorView;
|
||||
|
||||
@@ -50,11 +49,6 @@ pub(crate) struct BottomPane<'a> {
|
||||
is_task_running: bool,
|
||||
ctrl_c_quit_hint: bool,
|
||||
|
||||
/// Optional live, multi‑line status/"live cell" rendered directly above
|
||||
/// the composer while a task is running. Unlike `active_view`, this does
|
||||
/// not replace the composer; it augments it.
|
||||
live_status: Option<StatusIndicatorWidget>,
|
||||
|
||||
/// True if the active view is the StatusIndicatorView that replaces the
|
||||
/// composer during a running task.
|
||||
status_view_active: bool,
|
||||
@@ -81,27 +75,18 @@ impl BottomPane<'_> {
|
||||
has_input_focus: params.has_input_focus,
|
||||
is_task_running: false,
|
||||
ctrl_c_quit_hint: false,
|
||||
live_status: None,
|
||||
status_view_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn desired_height(&self, width: u16) -> u16 {
|
||||
let overlay_status_h = self
|
||||
.live_status
|
||||
.as_ref()
|
||||
.map(|s| s.desired_height(width))
|
||||
.unwrap_or(0);
|
||||
|
||||
let view_height = if let Some(view) = self.active_view.as_ref() {
|
||||
view.desired_height(width)
|
||||
} else {
|
||||
self.composer.desired_height(width)
|
||||
};
|
||||
|
||||
overlay_status_h
|
||||
.saturating_add(view_height)
|
||||
.saturating_add(Self::BOTTOM_PAD_LINES)
|
||||
view_height.saturating_add(Self::BOTTOM_PAD_LINES)
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
@@ -182,44 +167,6 @@ impl BottomPane<'_> {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Update the status indicator text. Prefer replacing the composer with
|
||||
/// the StatusIndicatorView so the input pane shows a single-line status
|
||||
/// like: `▌ Working waiting for model`.
|
||||
pub(crate) fn update_status_text(&mut self, text: String) {
|
||||
let mut handled_by_view = false;
|
||||
if let Some(view) = self.active_view.as_mut() {
|
||||
if matches!(
|
||||
view.update_status_text(text.clone()),
|
||||
bottom_pane_view::ConditionalUpdate::NeedsRedraw
|
||||
) {
|
||||
handled_by_view = true;
|
||||
}
|
||||
} else {
|
||||
let mut v = StatusIndicatorView::new(self.app_event_tx.clone());
|
||||
v.update_text(text.clone());
|
||||
self.active_view = Some(Box::new(v));
|
||||
self.status_view_active = true;
|
||||
handled_by_view = true;
|
||||
}
|
||||
|
||||
// Fallback: if the current active view did not consume status updates
|
||||
// and no modal view is active, present an overlay above the composer.
|
||||
// If a modal is active, do NOT render the overlay to avoid drawing
|
||||
// over the dialog.
|
||||
if !handled_by_view && self.active_view.is_none() {
|
||||
if self.live_status.is_none() {
|
||||
self.live_status = Some(StatusIndicatorWidget::new(self.app_event_tx.clone()));
|
||||
}
|
||||
if let Some(status) = &mut self.live_status {
|
||||
status.update_text(text);
|
||||
}
|
||||
} else if !handled_by_view {
|
||||
// Ensure any previous overlay is cleared when a modal becomes active.
|
||||
self.live_status = None;
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn show_ctrl_c_quit_hint(&mut self) {
|
||||
self.ctrl_c_quit_hint = true;
|
||||
self.composer
|
||||
@@ -252,7 +199,6 @@ impl BottomPane<'_> {
|
||||
}
|
||||
self.request_redraw();
|
||||
} else {
|
||||
self.live_status = None;
|
||||
// Drop the status view when a task completes, but keep other
|
||||
// modal views (e.g. approval dialogs).
|
||||
if let Some(mut view) = self.active_view.take() {
|
||||
@@ -302,8 +248,6 @@ impl BottomPane<'_> {
|
||||
// Otherwise create a new approval modal overlay.
|
||||
let modal = ApprovalModalView::new(request, self.app_event_tx.clone());
|
||||
self.active_view = Some(Box::new(modal));
|
||||
// Hide any overlay status while a modal is visible.
|
||||
self.live_status = None;
|
||||
self.status_view_active = false;
|
||||
self.request_redraw()
|
||||
}
|
||||
@@ -342,46 +286,31 @@ impl BottomPane<'_> {
|
||||
|
||||
impl WidgetRef for &BottomPane<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut y_offset = 0u16;
|
||||
if let Some(status) = &self.live_status {
|
||||
let live_h = status
|
||||
.desired_height(area.width)
|
||||
.min(area.height.saturating_sub(y_offset));
|
||||
if live_h > 0 {
|
||||
let live_rect = Rect {
|
||||
x: area.x,
|
||||
y: area.y + y_offset,
|
||||
width: area.width,
|
||||
height: live_h,
|
||||
};
|
||||
status.render_ref(live_rect, buf);
|
||||
y_offset = y_offset.saturating_add(live_h);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(view) = &self.active_view {
|
||||
if y_offset < area.height {
|
||||
// Reserve bottom padding lines; keep at least 1 line for the view.
|
||||
let avail = area.height - y_offset;
|
||||
// Reserve bottom padding lines; keep at least 1 line for the view.
|
||||
let avail = area.height;
|
||||
if avail > 0 {
|
||||
let pad = BottomPane::BOTTOM_PAD_LINES.min(avail.saturating_sub(1));
|
||||
let view_rect = Rect {
|
||||
x: area.x,
|
||||
y: area.y + y_offset,
|
||||
y: area.y,
|
||||
width: area.width,
|
||||
height: avail - pad,
|
||||
};
|
||||
view.render(view_rect, buf);
|
||||
}
|
||||
} else if y_offset < area.height {
|
||||
let composer_rect = Rect {
|
||||
x: area.x,
|
||||
y: area.y + y_offset,
|
||||
width: area.width,
|
||||
// Reserve bottom padding
|
||||
height: (area.height - y_offset)
|
||||
- BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1)),
|
||||
};
|
||||
(&self.composer).render_ref(composer_rect, buf);
|
||||
} else {
|
||||
let avail = area.height;
|
||||
if avail > 0 {
|
||||
let composer_rect = Rect {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: area.width,
|
||||
// Reserve bottom padding
|
||||
height: avail - BottomPane::BOTTOM_PAD_LINES.min(avail.saturating_sub(1)),
|
||||
};
|
||||
(&self.composer).render_ref(composer_rect, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,10 +360,8 @@ mod tests {
|
||||
|
||||
// Create an approval modal (active view).
|
||||
pane.push_approval_request(exec_request());
|
||||
// Attempt to update status; this should NOT create an overlay while modal is visible.
|
||||
pane.update_status_text("running command".to_string());
|
||||
|
||||
// Render and verify the top row does not include the Working header overlay.
|
||||
// Render and verify the top row does not include an overlay.
|
||||
let area = Rect::new(0, 0, 60, 6);
|
||||
let mut buf = Buffer::empty(area);
|
||||
(&pane).render_ref(area, &mut buf);
|
||||
@@ -445,7 +372,7 @@ mod tests {
|
||||
}
|
||||
assert!(
|
||||
!r0.contains("Working"),
|
||||
"overlay Working header should not render above modal"
|
||||
"overlay should not render above modal"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -461,7 +388,6 @@ mod tests {
|
||||
|
||||
// Start a running task so the status indicator replaces the composer.
|
||||
pane.set_task_running(true);
|
||||
pane.update_status_text("waiting for model".to_string());
|
||||
|
||||
// Push an approval modal (e.g., command approval) which should hide the status view.
|
||||
pane.push_approval_request(exec_request());
|
||||
@@ -511,11 +437,6 @@ mod tests {
|
||||
|
||||
// Begin a task: show initial status.
|
||||
pane.set_task_running(true);
|
||||
pane.update_status_text("waiting for model".to_string());
|
||||
|
||||
// As a long-running command begins (post-approval), ensure the status
|
||||
// indicator is visible while we wait for the command to run.
|
||||
pane.update_status_text("running command".to_string());
|
||||
|
||||
// Allow some frames so the animation thread ticks.
|
||||
std::thread::sleep(std::time::Duration::from_millis(120));
|
||||
@@ -547,7 +468,6 @@ mod tests {
|
||||
|
||||
// Activate spinner (status view replaces composer) with no live ring.
|
||||
pane.set_task_running(true);
|
||||
pane.update_status_text("waiting for model".to_string());
|
||||
|
||||
// Use height == desired_height; expect 1 status row at top and 2 bottom padding rows.
|
||||
let height = pane.desired_height(30);
|
||||
@@ -598,7 +518,6 @@ mod tests {
|
||||
});
|
||||
|
||||
pane.set_task_running(true);
|
||||
pane.update_status_text("waiting for model".to_string());
|
||||
|
||||
// Height=2 → pad shrinks to 1; bottom row is blank, top row has spinner.
|
||||
let area2 = Rect::new(0, 0, 20, 2);
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::bottom_pane::BottomPane;
|
||||
use crate::status_indicator_widget::StatusIndicatorWidget;
|
||||
|
||||
use super::BottomPaneView;
|
||||
use super::bottom_pane_view::ConditionalUpdate;
|
||||
|
||||
pub(crate) struct StatusIndicatorView {
|
||||
view: StatusIndicatorWidget,
|
||||
@@ -27,11 +26,6 @@ impl StatusIndicatorView {
|
||||
}
|
||||
|
||||
impl BottomPaneView<'_> for StatusIndicatorView {
|
||||
fn update_status_text(&mut self, text: String) -> ConditionalUpdate {
|
||||
self.update_text(text);
|
||||
ConditionalUpdate::NeedsRedraw
|
||||
}
|
||||
|
||||
fn should_hide_when_task_is_done(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ use crate::bottom_pane::BottomPaneParams;
|
||||
use crate::bottom_pane::CancellationEvent;
|
||||
use crate::bottom_pane::InputResult;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::history_cell;
|
||||
use crate::history_cell::CommandOutput;
|
||||
use crate::history_cell::ExecCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::PatchEventType;
|
||||
// streaming internals are provided by crate::streaming and crate::markdown_stream
|
||||
@@ -68,7 +70,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
app_event_tx: AppEventSender,
|
||||
codex_op_tx: UnboundedSender<Op>,
|
||||
bottom_pane: BottomPane<'a>,
|
||||
active_exec_cell: Option<HistoryCell>,
|
||||
active_exec_cell: Option<ExecCell>,
|
||||
config: Config,
|
||||
initial_user_message: Option<UserMessage>,
|
||||
total_token_usage: TokenUsage,
|
||||
@@ -123,7 +125,7 @@ impl ChatWidget<'_> {
|
||||
fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) {
|
||||
self.bottom_pane
|
||||
.set_history_metadata(event.history_log_id, event.history_entry_count);
|
||||
self.add_to_history(HistoryCell::new_session_info(&self.config, event, true));
|
||||
self.add_to_history(&history_cell::new_session_info(&self.config, event, true));
|
||||
if let Some(user_message) = self.initial_user_message.take() {
|
||||
self.submit_user_message(user_message);
|
||||
}
|
||||
@@ -164,7 +166,6 @@ impl ChatWidget<'_> {
|
||||
fn on_task_started(&mut self) {
|
||||
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.set_waiting_for_model_status();
|
||||
self.stream.reset_headers_for_new_turn();
|
||||
self.last_stream_kind = None;
|
||||
self.mark_needs_redraw();
|
||||
@@ -195,14 +196,14 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
self.add_to_history(HistoryCell::new_error_event(message));
|
||||
self.add_to_history(&history_cell::new_error_event(message));
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.stream.clear_all();
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
|
||||
fn on_plan_update(&mut self, update: codex_core::plan_tool::UpdatePlanArgs) {
|
||||
self.add_to_history(HistoryCell::new_plan_update(update));
|
||||
self.add_to_history(&history_cell::new_plan_update(update));
|
||||
}
|
||||
|
||||
fn on_exec_approval_request(&mut self, id: String, ev: ExecApprovalRequestEvent) {
|
||||
@@ -237,7 +238,7 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) {
|
||||
self.add_to_history(HistoryCell::new_patch_event(
|
||||
self.add_to_history(&history_cell::new_patch_event(
|
||||
PatchEventType::ApplyBegin {
|
||||
auto_approved: event.auto_approved,
|
||||
},
|
||||
@@ -336,16 +337,9 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_waiting_for_model_status(&mut self) {
|
||||
self.bottom_pane
|
||||
.update_status_text("waiting for model".to_string());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn handle_streaming_delta(&mut self, kind: StreamKind, delta: String) {
|
||||
let sink = AppEventHistorySink(self.app_event_tx.clone());
|
||||
self.set_waiting_for_model_status();
|
||||
self.stream.begin(kind, &sink);
|
||||
self.last_stream_kind = Some(kind);
|
||||
self.stream.push_and_maybe_commit(&delta, &sink);
|
||||
@@ -372,7 +366,7 @@ impl ChatWidget<'_> {
|
||||
self.active_exec_cell = None;
|
||||
let pending = std::mem::take(&mut self.pending_exec_completions);
|
||||
for (command, parsed, output) in pending {
|
||||
self.add_to_history(HistoryCell::new_completed_exec_command(
|
||||
self.add_to_history(&history_cell::new_completed_exec_command(
|
||||
command, parsed, output,
|
||||
));
|
||||
}
|
||||
@@ -384,9 +378,9 @@ impl ChatWidget<'_> {
|
||||
event: codex_core::protocol::PatchApplyEndEvent,
|
||||
) {
|
||||
if event.success {
|
||||
self.add_to_history(HistoryCell::new_patch_apply_success(event.stdout));
|
||||
self.add_to_history(&history_cell::new_patch_apply_success(event.stdout));
|
||||
} else {
|
||||
self.add_to_history(HistoryCell::new_patch_apply_failure(event.stderr));
|
||||
self.add_to_history(&history_cell::new_patch_apply_failure(event.stderr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +396,7 @@ impl ChatWidget<'_> {
|
||||
.map(|r| format!("\n{r}"))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
self.add_to_history(HistoryCell::new_background_event(text));
|
||||
self.add_to_history(&history_cell::new_background_event(text));
|
||||
|
||||
let request = ApprovalRequest::Exec {
|
||||
id,
|
||||
@@ -419,7 +413,7 @@ impl ChatWidget<'_> {
|
||||
ev: ApplyPatchApprovalRequestEvent,
|
||||
) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.add_to_history(HistoryCell::new_patch_event(
|
||||
self.add_to_history(&history_cell::new_patch_event(
|
||||
PatchEventType::ApprovalRequest,
|
||||
ev.changes.clone(),
|
||||
));
|
||||
@@ -435,8 +429,6 @@ impl ChatWidget<'_> {
|
||||
|
||||
pub(crate) fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) {
|
||||
// Ensure the status indicator is visible while the command runs.
|
||||
self.bottom_pane
|
||||
.update_status_text("running command".to_string());
|
||||
self.running_commands.insert(
|
||||
ev.call_id.clone(),
|
||||
RunningCommand {
|
||||
@@ -446,11 +438,11 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
// Accumulate parsed commands into a single active Exec cell so they stack
|
||||
match self.active_exec_cell.as_mut() {
|
||||
Some(HistoryCell::Exec(exec)) => {
|
||||
Some(exec) => {
|
||||
exec.parsed.extend(ev.parsed_cmd);
|
||||
}
|
||||
_ => {
|
||||
self.active_exec_cell = Some(HistoryCell::new_active_exec_command(
|
||||
self.active_exec_cell = Some(history_cell::new_active_exec_command(
|
||||
ev.command,
|
||||
ev.parsed_cmd,
|
||||
));
|
||||
@@ -463,11 +455,11 @@ impl ChatWidget<'_> {
|
||||
|
||||
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.add_to_history(HistoryCell::new_active_mcp_tool_call(ev.invocation));
|
||||
self.add_to_history(&history_cell::new_active_mcp_tool_call(ev.invocation));
|
||||
}
|
||||
pub(crate) fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.add_to_history(HistoryCell::new_completed_mcp_tool_call(
|
||||
self.add_to_history(&*history_cell::new_completed_mcp_tool_call(
|
||||
80,
|
||||
ev.invocation,
|
||||
ev.duration,
|
||||
@@ -564,14 +556,14 @@ impl ChatWidget<'_> {
|
||||
fn flush_active_exec_cell(&mut self) {
|
||||
if let Some(active) = self.active_exec_cell.take() {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::InsertHistory(active.plain_lines()));
|
||||
.send(AppEvent::InsertHistory(active.display_lines()));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_to_history(&mut self, cell: HistoryCell) {
|
||||
fn add_to_history(&mut self, cell: &dyn HistoryCell) {
|
||||
self.flush_active_exec_cell();
|
||||
self.app_event_tx
|
||||
.send(AppEvent::InsertHistory(cell.plain_lines()));
|
||||
.send(AppEvent::InsertHistory(cell.display_lines()));
|
||||
}
|
||||
|
||||
fn submit_user_message(&mut self, user_message: UserMessage) {
|
||||
@@ -607,7 +599,7 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Only show the text portion in conversation history.
|
||||
if !text.is_empty() {
|
||||
self.add_to_history(HistoryCell::new_user_prompt(text.clone()));
|
||||
self.add_to_history(&history_cell::new_user_prompt(text.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,30 +660,23 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the live log preview while a task is running.
|
||||
pub(crate) fn update_latest_log(&mut self, line: String) {
|
||||
if self.bottom_pane.is_task_running() {
|
||||
self.bottom_pane.update_status_text(line);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_redraw(&mut self) {
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
|
||||
pub(crate) fn add_diff_output(&mut self, diff_output: String) {
|
||||
self.add_to_history(HistoryCell::new_diff_output(diff_output.clone()));
|
||||
self.add_to_history(&history_cell::new_diff_output(diff_output.clone()));
|
||||
}
|
||||
|
||||
pub(crate) fn add_status_output(&mut self) {
|
||||
self.add_to_history(HistoryCell::new_status_output(
|
||||
self.add_to_history(&history_cell::new_status_output(
|
||||
&self.config,
|
||||
&self.total_token_usage,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn add_prompts_output(&mut self) {
|
||||
self.add_to_history(HistoryCell::new_prompts_output());
|
||||
self.add_to_history(&history_cell::new_prompts_output());
|
||||
}
|
||||
|
||||
/// Forward file-search results to the bottom pane.
|
||||
|
||||
@@ -361,19 +361,22 @@ fn style_del() -> Style {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::text_block::TextBlock;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
fn snapshot_lines(name: &str, lines: Vec<RtLine<'static>>, width: u16, height: u16) {
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("terminal");
|
||||
let cell = HistoryCell::PendingPatch {
|
||||
view: TextBlock::new(lines),
|
||||
};
|
||||
terminal
|
||||
.draw(|f| f.render_widget_ref(&cell, f.area()))
|
||||
.draw(|f| {
|
||||
Paragraph::new(Text::from(lines))
|
||||
.wrap(Wrap { trim: false })
|
||||
.render_ref(f.area(), f.buffer_mut())
|
||||
})
|
||||
.expect("draw");
|
||||
assert_snapshot!(name, terminal.backend());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||
use log_layer::TuiLogLayer;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::PathBuf;
|
||||
use tracing::error;
|
||||
@@ -39,7 +38,6 @@ mod get_git_diff;
|
||||
mod history_cell;
|
||||
pub mod insert_history;
|
||||
pub mod live_wrap;
|
||||
mod log_layer;
|
||||
mod markdown;
|
||||
mod markdown_stream;
|
||||
pub mod onboarding;
|
||||
@@ -49,7 +47,6 @@ mod shimmer;
|
||||
mod slash_command;
|
||||
mod status_indicator_widget;
|
||||
mod streaming;
|
||||
mod text_block;
|
||||
mod text_formatting;
|
||||
mod tui;
|
||||
mod user_approval_widget;
|
||||
@@ -213,14 +210,7 @@ pub async fn run_main(
|
||||
.map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
|
||||
}
|
||||
|
||||
// Channel that carries formatted log lines to the UI.
|
||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
let tui_layer = TuiLogLayer::new(log_tx.clone(), 120).with_filter(env_filter());
|
||||
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(file_layer)
|
||||
.with(tui_layer)
|
||||
.try_init();
|
||||
let _ = tracing_subscriber::registry().with(file_layer).try_init();
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -254,7 +244,7 @@ pub async fn run_main(
|
||||
eprintln!("");
|
||||
}
|
||||
|
||||
run_ratatui_app(cli, config, should_show_trust_screen, log_rx)
|
||||
run_ratatui_app(cli, config, should_show_trust_screen)
|
||||
.map_err(|err| std::io::Error::other(err.to_string()))
|
||||
}
|
||||
|
||||
@@ -262,7 +252,6 @@ fn run_ratatui_app(
|
||||
cli: Cli,
|
||||
config: Config,
|
||||
should_show_trust_screen: bool,
|
||||
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
|
||||
color_eyre::install()?;
|
||||
|
||||
@@ -284,16 +273,6 @@ fn run_ratatui_app(
|
||||
let Cli { prompt, images, .. } = cli;
|
||||
let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen);
|
||||
|
||||
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
||||
{
|
||||
let app_event_tx = app.event_sender();
|
||||
tokio::spawn(async move {
|
||||
while let Some(line) = log_rx.recv().await {
|
||||
app_event_tx.send(crate::app_event::AppEvent::LatestLog(line));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let app_result = app.run(&mut terminal);
|
||||
let usage = app.token_usage();
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
//! Custom `tracing_subscriber` layer that forwards every formatted log event to the
|
||||
//! TUI so the status indicator can display the *latest* log line while a task is
|
||||
//! running.
|
||||
//!
|
||||
//! The layer is intentionally extremely small: we implement `on_event()` only and
|
||||
//! ignore spans/metadata because we only care about the already‑formatted output
|
||||
//! that the default `fmt` layer would print. We therefore borrow the same
|
||||
//! formatter (`tracing_subscriber::fmt::format::FmtSpan`) used by the default
|
||||
//! fmt layer so the text matches what is written to the log file.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tracing::Event;
|
||||
use tracing::Subscriber;
|
||||
use tracing::field::Field;
|
||||
use tracing::field::Visit;
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::layer::Context;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
/// Maximum characters forwarded to the TUI. Longer messages are truncated so the
|
||||
/// single‑line status indicator cannot overflow the viewport.
|
||||
pub struct TuiLogLayer {
|
||||
tx: UnboundedSender<String>,
|
||||
max_len: usize,
|
||||
}
|
||||
|
||||
impl TuiLogLayer {
|
||||
pub fn new(tx: UnboundedSender<String>, max_len: usize) -> Self {
|
||||
Self {
|
||||
tx,
|
||||
max_len: max_len.max(8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for TuiLogLayer
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
{
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
// Build a terse line like `[TRACE core::session] message …` by visiting
|
||||
// fields into a buffer. This avoids pulling in the heavyweight
|
||||
// formatter machinery.
|
||||
|
||||
struct Visitor<'a> {
|
||||
buf: &'a mut String,
|
||||
}
|
||||
|
||||
impl Visit for Visitor<'_> {
|
||||
fn record_debug(&mut self, _field: &Field, value: &dyn std::fmt::Debug) {
|
||||
let _ = write!(self.buf, " {value:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
let _ = write!(
|
||||
buf,
|
||||
"[{} {}]",
|
||||
event.metadata().level(),
|
||||
event.metadata().target()
|
||||
);
|
||||
|
||||
event.record(&mut Visitor { buf: &mut buf });
|
||||
|
||||
// `String::truncate` operates on UTF‑8 code‑point boundaries and will
|
||||
// panic if the provided index is not one. Because we limit the log
|
||||
// line by its **byte** length we can not guarantee that the index we
|
||||
// want to cut at happens to be on a boundary. Therefore we fall back
|
||||
// to a simple, boundary‑safe loop that pops complete characters until
|
||||
// the string is within the designated size.
|
||||
|
||||
if buf.len() > self.max_len {
|
||||
// Attempt direct truncate at the byte index. If that is not a
|
||||
// valid boundary we advance to the next one ( ≤3 bytes away ).
|
||||
if buf.is_char_boundary(self.max_len) {
|
||||
buf.truncate(self.max_len);
|
||||
} else {
|
||||
let mut idx = self.max_len;
|
||||
while idx < buf.len() && !buf.is_char_boundary(idx) {
|
||||
idx += 1;
|
||||
}
|
||||
buf.truncate(idx);
|
||||
}
|
||||
}
|
||||
|
||||
let sanitized = buf.replace(['\n', '\r'], " ");
|
||||
let _ = self.tx.send(sanitized);
|
||||
}
|
||||
}
|
||||
@@ -188,15 +188,6 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) {
|
||||
});
|
||||
LOGGER.write_json_line(value);
|
||||
}
|
||||
AppEvent::LatestLog(line) => {
|
||||
let value = json!({
|
||||
"ts": now_ts(),
|
||||
"dir": "to_tui",
|
||||
"kind": "log_line",
|
||||
"line": line,
|
||||
});
|
||||
LOGGER.write_json_line(value);
|
||||
}
|
||||
// Noise or control flow – record variant only
|
||||
other => {
|
||||
let value = json!({
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
use ratatui::prelude::*;
|
||||
|
||||
/// A simple widget that just displays a list of `Line`s via a `Paragraph`.
|
||||
/// This is the default rendering backend for most `HistoryCell` variants.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TextBlock {
|
||||
pub(crate) lines: Vec<Line<'static>>,
|
||||
}
|
||||
|
||||
impl TextBlock {
|
||||
pub(crate) fn new(lines: Vec<Line<'static>>) -> Self {
|
||||
Self { lines }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user