mirror of
https://github.com/openai/codex.git
synced 2026-02-28 03:33:57 +00:00
Compare commits
13 Commits
owen/small
...
codex/load
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0055796543 | ||
|
|
05bc61c428 | ||
|
|
ecaa77e499 | ||
|
|
6f1485d89c | ||
|
|
6b560a46be | ||
|
|
83726aebe6 | ||
|
|
dda7973531 | ||
|
|
d927cea570 | ||
|
|
bee23c7917 | ||
|
|
0ed71a0c3b | ||
|
|
e89f442a57 | ||
|
|
311bc6660d | ||
|
|
c800db5cd5 |
@@ -1010,11 +1010,11 @@ fn format_file_change_diff(change: &codex_protocol::protocol::FileChange) -> Str
|
||||
}
|
||||
|
||||
fn upsert_turn_item(items: &mut Vec<ThreadItem>, item: ThreadItem) {
|
||||
if let Some(index) = items
|
||||
.iter()
|
||||
.rposition(|existing_item| existing_item.id() == item.id())
|
||||
if let Some(existing_item) = items
|
||||
.iter_mut()
|
||||
.find(|existing_item| existing_item.id() == item.id())
|
||||
{
|
||||
items[index] = item;
|
||||
*existing_item = item;
|
||||
return;
|
||||
}
|
||||
items.push(item);
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::chatwidget::ChatWidget;
|
||||
use crate::chatwidget::DEFAULT_MODEL_DISPLAY_NAME;
|
||||
use crate::chatwidget::ExternalEditorState;
|
||||
use crate::cwd_prompt::CwdPromptAction;
|
||||
use crate::diff_render::DiffSummary;
|
||||
@@ -81,6 +82,8 @@ use color_eyre::eyre::WrapErr;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
@@ -723,6 +726,41 @@ impl App {
|
||||
.add_info_message(format!("Opened {url} in your browser."), None);
|
||||
}
|
||||
|
||||
fn insert_history_cell(&mut self, tui: &mut tui::Tui, cell: Arc<dyn HistoryCell>) {
|
||||
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
|
||||
t.insert_cell(cell.clone());
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
self.transcript_cells.push(cell.clone());
|
||||
let mut display = cell.display_lines(tui.terminal.last_known_screen_size.width);
|
||||
if !display.is_empty() {
|
||||
if !cell.is_stream_continuation() {
|
||||
if self.has_emitted_history_lines {
|
||||
display.insert(0, Line::from(""));
|
||||
} else {
|
||||
self.has_emitted_history_lines = true;
|
||||
}
|
||||
}
|
||||
if self.overlay.is_some() {
|
||||
self.deferred_history_lines.extend(display);
|
||||
} else {
|
||||
tui.insert_history_lines(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_startup_header(&mut self, tui: &mut tui::Tui) {
|
||||
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
|
||||
let header = Arc::new(history_cell::SessionHeaderHistoryCell::new_with_style(
|
||||
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
|
||||
placeholder_style,
|
||||
None,
|
||||
self.config.cwd.clone(),
|
||||
CODEX_CLI_VERSION,
|
||||
)) as Arc<dyn HistoryCell>;
|
||||
self.insert_history_cell(tui, header);
|
||||
}
|
||||
|
||||
fn clear_ui_header_lines_with_version(
|
||||
&self,
|
||||
width: u16,
|
||||
@@ -1199,6 +1237,7 @@ impl App {
|
||||
};
|
||||
self.chat_widget = ChatWidget::new(init, self.server.clone());
|
||||
self.reset_thread_event_state();
|
||||
self.insert_startup_header(tui);
|
||||
if let Some(summary) = summary {
|
||||
let mut lines: Vec<Line<'static>> = vec![summary.usage_line.clone().into()];
|
||||
if let Some(command) = summary.resume_command {
|
||||
@@ -1311,7 +1350,8 @@ impl App {
|
||||
use tokio_stream::StreamExt;
|
||||
let (app_event_tx, mut app_event_rx) = unbounded_channel();
|
||||
let app_event_tx = AppEventSender::new(app_event_tx);
|
||||
emit_project_config_warnings(&app_event_tx, &config);
|
||||
let should_insert_startup_header =
|
||||
matches!(&session_selection, SessionSelection::StartFresh);
|
||||
tui.set_notification_method(config.tui_notification_method);
|
||||
|
||||
let harness_overrides =
|
||||
@@ -1493,6 +1533,7 @@ impl App {
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
#[cfg(not(debug_assertions))]
|
||||
let upgrade_version = crate::updates::get_upgrade_version(&config);
|
||||
emit_project_config_warnings(&app_event_tx, &config);
|
||||
|
||||
let mut app = Self {
|
||||
server: thread_manager.clone(),
|
||||
@@ -1531,6 +1572,10 @@ impl App {
|
||||
pending_primary_events: VecDeque::new(),
|
||||
};
|
||||
|
||||
if should_insert_startup_header {
|
||||
app.insert_startup_header(tui);
|
||||
}
|
||||
|
||||
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -1883,29 +1928,7 @@ impl App {
|
||||
}
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let cell: Arc<dyn HistoryCell> = cell.into();
|
||||
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
|
||||
t.insert_cell(cell.clone());
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
self.transcript_cells.push(cell.clone());
|
||||
let mut display = cell.display_lines(tui.terminal.last_known_screen_size.width);
|
||||
if !display.is_empty() {
|
||||
// Only insert a separating blank line for new cells that are not
|
||||
// part of an ongoing stream. Streaming continuations should not
|
||||
// accrue extra blank lines between chunks.
|
||||
if !cell.is_stream_continuation() {
|
||||
if self.has_emitted_history_lines {
|
||||
display.insert(0, Line::from(""));
|
||||
} else {
|
||||
self.has_emitted_history_lines = true;
|
||||
}
|
||||
}
|
||||
if self.overlay.is_some() {
|
||||
self.deferred_history_lines.extend(display);
|
||||
} else {
|
||||
tui.insert_history_lines(display);
|
||||
}
|
||||
}
|
||||
self.insert_history_cell(tui, cell);
|
||||
}
|
||||
AppEvent::ApplyThreadRollback { num_turns } => {
|
||||
if self.apply_non_pending_thread_rollback(num_turns) {
|
||||
@@ -3064,6 +3087,55 @@ impl App {
|
||||
// thread, so unrelated shutdowns cannot consume this marker.
|
||||
self.pending_shutdown_exit_thread_id = None;
|
||||
}
|
||||
if let EventMsg::SessionConfigured(session) = &event.msg
|
||||
&& let Some(loading_header_idx) = self.transcript_cells.iter().rposition(|cell| {
|
||||
matches!(
|
||||
cell.as_ref()
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::SessionHeaderHistoryCell>(),
|
||||
Some(startup_header) if startup_header.is_loading_placeholder()
|
||||
)
|
||||
})
|
||||
{
|
||||
let cell = Arc::new(history_cell::SessionHeaderHistoryCell::new(
|
||||
session.model.clone(),
|
||||
session.reasoning_effort,
|
||||
session.cwd.clone(),
|
||||
CODEX_CLI_VERSION,
|
||||
)) as Arc<dyn HistoryCell>;
|
||||
self.transcript_cells[loading_header_idx] = cell.clone();
|
||||
if matches!(&self.overlay, Some(Overlay::Transcript(_))) {
|
||||
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
if loading_header_idx == 0 {
|
||||
let display = cell.display_lines(tui.terminal.last_known_screen_size.width);
|
||||
tui.replace_top_visible_history_lines(display)?;
|
||||
} else {
|
||||
self.clear_terminal_ui(tui, false)?;
|
||||
self.deferred_history_lines.clear();
|
||||
|
||||
let transcript_cells = self.transcript_cells.clone();
|
||||
for transcript_cell in transcript_cells {
|
||||
let mut display =
|
||||
transcript_cell.display_lines(tui.terminal.last_known_screen_size.width);
|
||||
if !display.is_empty() {
|
||||
if !transcript_cell.is_stream_continuation() {
|
||||
if self.has_emitted_history_lines {
|
||||
display.insert(0, Line::from(""));
|
||||
} else {
|
||||
self.has_emitted_history_lines = true;
|
||||
}
|
||||
}
|
||||
if self.overlay.is_some() {
|
||||
self.deferred_history_lines.extend(display);
|
||||
} else {
|
||||
tui.insert_history_lines(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.handle_codex_event_now(event);
|
||||
if self.backtrack_render_pending {
|
||||
tui.frame_requester().schedule_frame();
|
||||
@@ -3408,8 +3480,8 @@ mod tests {
|
||||
use crate::file_search::FileSearchManager;
|
||||
use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::SessionHeaderHistoryCell;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use crate::history_cell::new_session_info;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -3843,7 +3915,7 @@ mod tests {
|
||||
true,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
let make_header = |is_first| -> Arc<dyn HistoryCell> {
|
||||
let make_header = |_is_first| -> Arc<dyn HistoryCell> {
|
||||
let event = SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
@@ -3860,12 +3932,11 @@ mod tests {
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
};
|
||||
Arc::new(new_session_info(
|
||||
app.chat_widget.config_ref(),
|
||||
app.chat_widget.current_model(),
|
||||
event,
|
||||
is_first,
|
||||
None,
|
||||
Arc::new(SessionHeaderHistoryCell::new(
|
||||
event.model,
|
||||
event.reasoning_effort,
|
||||
event.cwd,
|
||||
crate::version::CODEX_CLI_VERSION,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
@@ -4340,7 +4411,7 @@ mod tests {
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
let make_header = |is_first| {
|
||||
let make_header = |_is_first| {
|
||||
let event = SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
@@ -4357,12 +4428,11 @@ mod tests {
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
};
|
||||
Arc::new(new_session_info(
|
||||
app.chat_widget.config_ref(),
|
||||
app.chat_widget.current_model(),
|
||||
event,
|
||||
is_first,
|
||||
None,
|
||||
Arc::new(SessionHeaderHistoryCell::new(
|
||||
event.model,
|
||||
event.reasoning_effort,
|
||||
event.cwd,
|
||||
crate::version::CODEX_CLI_VERSION,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::history_cell::SessionInfoCell;
|
||||
use crate::history_cell::SessionHeaderHistoryCell;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use crate::pager_overlay::Overlay;
|
||||
use crate::tui;
|
||||
@@ -639,7 +639,7 @@ fn nth_user_position(
|
||||
fn user_positions_iter(
|
||||
cells: &[Arc<dyn crate::history_cell::HistoryCell>],
|
||||
) -> impl Iterator<Item = usize> + '_ {
|
||||
let session_start_type = TypeId::of::<SessionInfoCell>();
|
||||
let session_start_type = TypeId::of::<SessionHeaderHistoryCell>();
|
||||
let user_type = TypeId::of::<UserHistoryCell>();
|
||||
let type_of = |cell: &Arc<dyn crate::history_cell::HistoryCell>| cell.as_any().type_id();
|
||||
|
||||
|
||||
@@ -146,8 +146,6 @@ use rand::Rng;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
@@ -157,7 +155,7 @@ use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
|
||||
pub(crate) const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
|
||||
const PLAN_IMPLEMENTATION_TITLE: &str = "Implement this plan?";
|
||||
const PLAN_IMPLEMENTATION_YES: &str = "Yes, implement this plan";
|
||||
const PLAN_IMPLEMENTATION_NO: &str = "No, stay in Plan mode";
|
||||
@@ -257,8 +255,6 @@ mod agent;
|
||||
use self::agent::spawn_agent;
|
||||
use self::agent::spawn_agent_from_existing;
|
||||
pub(crate) use self::agent::spawn_op_forwarder;
|
||||
mod session_header;
|
||||
use self::session_header::SessionHeader;
|
||||
mod skills;
|
||||
use self::skills::collect_tool_mentions;
|
||||
use self::skills::find_app_mentions;
|
||||
@@ -547,7 +543,6 @@ pub(crate) struct ChatWidget {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
models_manager: Arc<ModelsManager>,
|
||||
otel_manager: OtelManager,
|
||||
session_header: SessionHeader,
|
||||
initial_user_message: Option<UserMessage>,
|
||||
token_info: Option<TokenUsageInfo>,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap<String, RateLimitSnapshotDisplay>,
|
||||
@@ -1136,7 +1131,6 @@ impl ChatWidget {
|
||||
self.last_copyable_output = None;
|
||||
let forked_from_id = event.forked_from_id;
|
||||
let model_for_header = event.model.clone();
|
||||
self.session_header.set_model(&model_for_header);
|
||||
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
|
||||
Some(model_for_header.clone()),
|
||||
Some(event.reasoning_effort),
|
||||
@@ -1144,16 +1138,17 @@ impl ChatWidget {
|
||||
);
|
||||
self.refresh_model_display();
|
||||
self.sync_personality_command_enabled();
|
||||
let session_info_cell = history_cell::new_session_info(
|
||||
if let Some(session_info_body) = history_cell::new_session_info_body(
|
||||
&self.config,
|
||||
&model_for_header,
|
||||
event,
|
||||
&event,
|
||||
self.show_welcome_banner,
|
||||
self.auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.account_plan_type()),
|
||||
);
|
||||
self.apply_session_info_cell(session_info_cell);
|
||||
) {
|
||||
self.add_boxed_history(session_info_body);
|
||||
}
|
||||
|
||||
if let Some(messages) = initial_messages {
|
||||
self.replay_initial_messages(messages);
|
||||
@@ -2777,7 +2772,7 @@ impl ChatWidget {
|
||||
.and_then(|mask| mask.model.clone())
|
||||
.unwrap_or_else(|| model_for_header.clone());
|
||||
let fallback_default = Settings {
|
||||
model: header_model.clone(),
|
||||
model: header_model,
|
||||
reasoning_effort: None,
|
||||
developer_instructions: None,
|
||||
};
|
||||
@@ -2787,7 +2782,7 @@ impl ChatWidget {
|
||||
settings: fallback_default,
|
||||
};
|
||||
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(&config));
|
||||
let active_cell = None;
|
||||
|
||||
let current_cwd = Some(config.cwd.clone());
|
||||
let queued_message_edit_binding =
|
||||
@@ -2816,7 +2811,6 @@ impl ChatWidget {
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap::new(),
|
||||
@@ -2954,7 +2948,7 @@ impl ChatWidget {
|
||||
.and_then(|mask| mask.model.clone())
|
||||
.unwrap_or_else(|| model_for_header.clone());
|
||||
let fallback_default = Settings {
|
||||
model: header_model.clone(),
|
||||
model: header_model,
|
||||
reasoning_effort: None,
|
||||
developer_instructions: None,
|
||||
};
|
||||
@@ -2964,7 +2958,7 @@ impl ChatWidget {
|
||||
settings: fallback_default,
|
||||
};
|
||||
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(&config));
|
||||
let active_cell = None;
|
||||
let current_cwd = Some(config.cwd.clone());
|
||||
|
||||
let queued_message_edit_binding =
|
||||
@@ -2993,7 +2987,6 @@ impl ChatWidget {
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap::new(),
|
||||
@@ -3111,6 +3104,7 @@ impl ChatWidget {
|
||||
let header_model = model
|
||||
.clone()
|
||||
.unwrap_or_else(|| session_configured.model.clone());
|
||||
let header_reasoning_effort = session_configured.reasoning_effort;
|
||||
let active_collaboration_mask =
|
||||
Self::initial_collaboration_mask(&config, models_manager.as_ref(), model_override);
|
||||
let header_model = active_collaboration_mask
|
||||
@@ -3159,7 +3153,6 @@ impl ChatWidget {
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap::new(),
|
||||
@@ -3253,6 +3246,17 @@ impl ChatWidget {
|
||||
);
|
||||
widget.update_collaboration_mode_indicator();
|
||||
|
||||
widget
|
||||
.app_event_tx
|
||||
.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::SessionHeaderHistoryCell::new(
|
||||
header_model,
|
||||
header_reasoning_effort,
|
||||
widget.config.cwd.clone(),
|
||||
CODEX_CLI_VERSION,
|
||||
),
|
||||
)));
|
||||
|
||||
widget
|
||||
}
|
||||
|
||||
@@ -3962,15 +3966,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>) {
|
||||
// Keep the placeholder session header as the active cell until real session info arrives,
|
||||
// so we can merge headers instead of committing a duplicate box to history.
|
||||
let keep_placeholder_header_active = !self.is_session_configured()
|
||||
&& self
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.as_any().is::<history_cell::SessionHeaderHistoryCell>());
|
||||
|
||||
if !keep_placeholder_header_active && !cell.display_lines(u16::MAX).is_empty() {
|
||||
if cell.has_visible_display_lines() {
|
||||
// Only break exec grouping if the cell renders visible lines.
|
||||
self.flush_active_cell();
|
||||
self.needs_final_message_separator = true;
|
||||
@@ -6958,8 +6954,6 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn refresh_model_display(&mut self) {
|
||||
let effective = self.effective_collaboration_mode();
|
||||
self.session_header.set_model(effective.model());
|
||||
// Keep composer paste affordances aligned with the currently effective model.
|
||||
self.sync_image_paste_enabled();
|
||||
}
|
||||
@@ -7090,46 +7084,6 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a placeholder header cell while the session is configuring.
|
||||
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell> {
|
||||
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
|
||||
Box::new(history_cell::SessionHeaderHistoryCell::new_with_style(
|
||||
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
|
||||
placeholder_style,
|
||||
None,
|
||||
config.cwd.clone(),
|
||||
CODEX_CLI_VERSION,
|
||||
))
|
||||
}
|
||||
|
||||
/// Merge the real session info cell with any placeholder header to avoid double boxes.
|
||||
fn apply_session_info_cell(&mut self, cell: history_cell::SessionInfoCell) {
|
||||
let mut session_info_cell = Some(Box::new(cell) as Box<dyn HistoryCell>);
|
||||
let merged_header = if let Some(active) = self.active_cell.take() {
|
||||
if active
|
||||
.as_any()
|
||||
.is::<history_cell::SessionHeaderHistoryCell>()
|
||||
{
|
||||
// Reuse the existing placeholder header to avoid rendering two boxes.
|
||||
if let Some(cell) = session_info_cell.take() {
|
||||
self.active_cell = Some(cell);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
self.active_cell = Some(active);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.flush_active_cell();
|
||||
|
||||
if !merged_header && let Some(cell) = session_info_cell {
|
||||
self.add_boxed_history(cell);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_info_message(&mut self, message: String, hint: Option<String>) {
|
||||
self.add_to_history(history_cell::new_info_event(message, hint));
|
||||
self.request_redraw();
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
pub(crate) struct SessionHeader {
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl SessionHeader {
|
||||
pub(crate) fn new(model: String) -> Self {
|
||||
Self { model }
|
||||
}
|
||||
|
||||
/// Updates the header's model text.
|
||||
pub(crate) fn set_model(&mut self, model: &str) {
|
||||
if self.model != model {
|
||||
self.model = model.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1668,7 +1668,6 @@ async fn make_chatwidget_manual(
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(resolved_model.clone()),
|
||||
initial_user_message: None,
|
||||
token_info: None,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap::new(),
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
//! bumps the active-cell revision tracked by `ChatWidget`, so the cache key changes whenever the
|
||||
//! rendered transcript output can change.
|
||||
|
||||
use crate::chatwidget::DEFAULT_MODEL_DISPLAY_NAME;
|
||||
use crate::diff_render::create_diff_summary;
|
||||
use crate::diff_render::display_path_for;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
@@ -95,6 +96,11 @@ pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any {
|
||||
/// Returns the logical lines for the main chat viewport.
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>>;
|
||||
|
||||
/// Returns whether this cell renders any visible display lines when width is unconstrained.
|
||||
fn has_visible_display_lines(&self) -> bool {
|
||||
!self.display_lines(u16::MAX).is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of viewport rows needed to render this cell.
|
||||
///
|
||||
/// The default delegates to `Paragraph::line_count` with
|
||||
@@ -1019,43 +1025,30 @@ impl HistoryCell for TooltipHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionInfoCell(CompositeHistoryCell);
|
||||
pub(crate) fn new_session_info_body(
|
||||
config: &Config,
|
||||
requested_model: &str,
|
||||
event: &SessionConfiguredEvent,
|
||||
is_first_event: bool,
|
||||
auth_plan: Option<PlanType>,
|
||||
) -> Option<Box<dyn HistoryCell>> {
|
||||
let parts = session_info_body_parts(config, requested_model, event, is_first_event, auth_plan);
|
||||
|
||||
impl HistoryCell for SessionInfoCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
self.0.display_lines(width)
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.0.desired_height(width)
|
||||
}
|
||||
|
||||
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
self.0.transcript_lines(width)
|
||||
match parts.len() {
|
||||
0 => None,
|
||||
1 => parts.into_iter().next(),
|
||||
_ => Some(Box::new(CompositeHistoryCell::new(parts))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_session_info(
|
||||
fn session_info_body_parts(
|
||||
config: &Config,
|
||||
requested_model: &str,
|
||||
event: SessionConfiguredEvent,
|
||||
event: &SessionConfiguredEvent,
|
||||
is_first_event: bool,
|
||||
auth_plan: Option<PlanType>,
|
||||
) -> SessionInfoCell {
|
||||
let SessionConfiguredEvent {
|
||||
model,
|
||||
reasoning_effort,
|
||||
..
|
||||
} = event;
|
||||
// Header box rendered as history (so it appears at the very top)
|
||||
let header = SessionHeaderHistoryCell::new(
|
||||
model.clone(),
|
||||
reasoning_effort,
|
||||
config.cwd.clone(),
|
||||
CODEX_CLI_VERSION,
|
||||
);
|
||||
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
|
||||
) -> Vec<Box<dyn HistoryCell>> {
|
||||
let mut parts: Vec<Box<dyn HistoryCell>> = Vec::new();
|
||||
|
||||
if is_first_event {
|
||||
// Help lines below the header (new copy and list)
|
||||
@@ -1098,17 +1091,17 @@ pub(crate) fn new_session_info(
|
||||
{
|
||||
parts.push(Box::new(tooltips));
|
||||
}
|
||||
if requested_model != model {
|
||||
if requested_model != event.model {
|
||||
let lines = vec![
|
||||
"model changed:".magenta().bold().into(),
|
||||
format!("requested: {requested_model}").into(),
|
||||
format!("used: {model}").into(),
|
||||
format!("used: {}", event.model).into(),
|
||||
];
|
||||
parts.push(Box::new(PlainHistoryCell { lines }));
|
||||
}
|
||||
}
|
||||
|
||||
SessionInfoCell(CompositeHistoryCell { parts })
|
||||
parts
|
||||
}
|
||||
|
||||
pub(crate) fn new_user_prompt(
|
||||
@@ -1203,6 +1196,10 @@ impl SessionHeaderHistoryCell {
|
||||
ReasoningEffortConfig::None => "none",
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_loading_placeholder(&self) -> bool {
|
||||
self.model == DEFAULT_MODEL_DISPLAY_NAME
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryCell for SessionHeaderHistoryCell {
|
||||
|
||||
@@ -126,43 +126,7 @@ where
|
||||
|
||||
for line in wrapped {
|
||||
queue!(writer, Print("\r\n"))?;
|
||||
// URL lines can be wider than the terminal and will
|
||||
// character-wrap onto continuation rows. Pre-clear those rows
|
||||
// so stale content from a previously longer line is erased.
|
||||
let physical_rows = line.width().max(1).div_ceil(wrap_width);
|
||||
if physical_rows > 1 {
|
||||
queue!(writer, SavePosition)?;
|
||||
for _ in 1..physical_rows {
|
||||
queue!(writer, MoveDown(1), MoveToColumn(0))?;
|
||||
queue!(writer, Clear(ClearType::UntilNewLine))?;
|
||||
}
|
||||
queue!(writer, RestorePosition)?;
|
||||
}
|
||||
queue!(
|
||||
writer,
|
||||
SetColors(Colors::new(
|
||||
line.style
|
||||
.fg
|
||||
.map(std::convert::Into::into)
|
||||
.unwrap_or(CColor::Reset),
|
||||
line.style
|
||||
.bg
|
||||
.map(std::convert::Into::into)
|
||||
.unwrap_or(CColor::Reset)
|
||||
))
|
||||
)?;
|
||||
queue!(writer, Clear(ClearType::UntilNewLine))?;
|
||||
// Merge line-level style into each span so that ANSI colors reflect
|
||||
// line styles (e.g., blockquotes with green fg).
|
||||
let merged_spans: Vec<Span> = line
|
||||
.spans
|
||||
.iter()
|
||||
.map(|s| Span {
|
||||
style: s.style.patch(line.style),
|
||||
content: s.content.clone(),
|
||||
})
|
||||
.collect();
|
||||
write_spans(writer, merged_spans.iter())?;
|
||||
write_line(writer, &line, wrap_width)?;
|
||||
}
|
||||
|
||||
queue!(writer, ResetScrollRegion)?;
|
||||
@@ -181,6 +145,36 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn replace_top_visible_history_lines<B>(
|
||||
terminal: &mut crate::custom_terminal::Terminal<B>,
|
||||
lines: Vec<Line>,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
B: Backend + Write,
|
||||
{
|
||||
if lines.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let top = terminal
|
||||
.viewport_area
|
||||
.top()
|
||||
.saturating_sub(terminal.visible_history_rows());
|
||||
let wrap_width = terminal.viewport_area.width.max(1) as usize;
|
||||
let last_cursor_pos = terminal.last_known_cursor_pos;
|
||||
let writer = terminal.backend_mut();
|
||||
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
let y = top.saturating_add(index as u16);
|
||||
queue!(writer, MoveTo(0, y))?;
|
||||
write_line(writer, line, wrap_width)?;
|
||||
}
|
||||
|
||||
queue!(writer, MoveTo(last_cursor_pos.x, last_cursor_pos.y))?;
|
||||
std::io::Write::flush(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetScrollRegion(pub std::ops::Range<u16>);
|
||||
|
||||
@@ -329,6 +323,43 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn write_line(mut writer: &mut impl Write, line: &Line<'_>, wrap_width: usize) -> io::Result<()> {
|
||||
// URL lines can be wider than the terminal and will character-wrap onto continuation rows.
|
||||
// Pre-clear those rows so stale content from a previously longer line is erased.
|
||||
let physical_rows = line.width().max(1).div_ceil(wrap_width);
|
||||
if physical_rows > 1 {
|
||||
queue!(writer, SavePosition)?;
|
||||
for _ in 1..physical_rows {
|
||||
queue!(writer, MoveDown(1), MoveToColumn(0))?;
|
||||
queue!(writer, Clear(ClearType::UntilNewLine))?;
|
||||
}
|
||||
queue!(writer, RestorePosition)?;
|
||||
}
|
||||
queue!(
|
||||
writer,
|
||||
SetColors(Colors::new(
|
||||
line.style
|
||||
.fg
|
||||
.map(std::convert::Into::into)
|
||||
.unwrap_or(CColor::Reset),
|
||||
line.style
|
||||
.bg
|
||||
.map(std::convert::Into::into)
|
||||
.unwrap_or(CColor::Reset)
|
||||
))
|
||||
)?;
|
||||
queue!(writer, Clear(ClearType::UntilNewLine))?;
|
||||
let merged_spans: Vec<Span> = line
|
||||
.spans
|
||||
.iter()
|
||||
.map(|s| Span {
|
||||
style: s.style.patch(line.style),
|
||||
content: s.content.clone(),
|
||||
})
|
||||
.collect();
|
||||
write_spans(&mut writer, merged_spans.iter())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -445,6 +445,16 @@ impl Tui {
|
||||
self.frame_requester().schedule_frame();
|
||||
}
|
||||
|
||||
pub fn replace_top_visible_history_lines(&mut self, lines: Vec<Line<'static>>) -> Result<()> {
|
||||
let line_count = lines.len();
|
||||
if self.terminal.visible_history_rows() >= line_count as u16 {
|
||||
crate::insert_history::replace_top_visible_history_lines(&mut self.terminal, lines)?;
|
||||
} else if self.pending_history_lines.len() >= line_count {
|
||||
self.pending_history_lines.splice(0..line_count, lines);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_pending_history_lines(&mut self) {
|
||||
self.pending_history_lines.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user