Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex-rs/core/src/unified_exec/process_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async fn output_collection_stays_bounded_across_repeated_drains() {
Instant::now() + Duration::from_secs(5),
);
let produce = async {
for byte in [b'a', b'b', b'c'] {
for byte in *b"abc" {
output_buffer.lock().await.push_chunk(
vec![byte; crate::unified_exec::UNIFIED_EXEC_OUTPUT_MAX_BYTES],
);
Expand All @@ -173,7 +173,7 @@ async fn output_collection_stays_bounded_across_repeated_drains() {

let (collected, ()) = tokio::join!(collect, produce);
let mut expected = HeadTailBuffer::default();
for byte in [b'a', b'b', b'c'] {
for byte in *b"abc" {
expected.push_chunk(vec![
byte;
crate::unified_exec::UNIFIED_EXEC_OUTPUT_MAX_BYTES
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/tui/src/app/thread_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,10 @@ impl App {
if let Some(turn_error) =
active_turn_not_steerable_turn_error(&error)
{
if !self.chat_widget.enqueue_rejected_steer() {
if !self
.chat_widget
.enqueue_rejected_steer_matching_items(items)
{
self.chat_widget.add_error_message(turn_error.message);
}
return Ok(true);
Expand Down
24 changes: 24 additions & 0 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@ pub(crate) struct ComposerDraftSnapshot {
pub(crate) remote_image_urls: Vec<String>,
pub(crate) mention_bindings: Vec<MentionBinding>,
pub(crate) pending_pastes: Vec<(String, String)>,
/// Caret position, kept private so the snapshot can only round-trip through
/// [`ChatComposer::restore_draft_snapshot`].
cursor: usize,
}

const FOOTER_SPACING_HEIGHT: u16 = 0;
Expand Down Expand Up @@ -1484,9 +1487,30 @@ impl ChatComposer {
remote_image_urls: self.remote_image_urls(),
mention_bindings: self.mention_bindings(),
pending_pastes: self.pending_pastes(),
cursor: self.current_cursor(),
}
}

/// Put a previously captured draft back into the composer, caret included.
///
/// Used to undo a speculative composer write when the submission it belonged to was
/// refused, so the user's in-progress draft is not clobbered.
pub(crate) fn restore_draft_snapshot(&mut self, snapshot: ComposerDraftSnapshot) {
self.restore_draft(ComposerDraft {
text: snapshot.text,
text_elements: snapshot.text_elements,
local_image_paths: snapshot
.local_images
.into_iter()
.map(|image| image.path)
.collect(),
remote_image_urls: snapshot.remote_image_urls,
mention_bindings: snapshot.mention_bindings,
pending_pastes: snapshot.pending_pastes,
cursor: snapshot.cursor,
});
}

#[cfg(test)]
pub(crate) fn local_image_paths(&self) -> Vec<PathBuf> {
self.attachments.local_image_paths()
Expand Down
21 changes: 21 additions & 0 deletions codex-rs/tui/src/bottom_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ impl BottomPane {
self.request_redraw();
}

/// Update the key hint for merging every queued follow-up into the active turn so it
/// matches the binding that `ChatWidget` actually listens for.
pub(crate) fn set_queued_message_flush_binding(&mut self, binding: Option<KeyBinding>) {
self.pending_input_preview.set_flush_binding(binding);
self.request_redraw();
}

pub(crate) fn set_vim_enabled(&mut self, enabled: bool) {
self.composer.set_vim_enabled(enabled);
self.request_redraw();
Expand Down Expand Up @@ -858,6 +865,14 @@ impl BottomPane {
self.composer.draft_snapshot()
}

pub(crate) fn restore_composer_draft_snapshot(
&mut self,
snapshot: chat_composer::ComposerDraftSnapshot,
) {
self.composer.restore_draft_snapshot(snapshot);
self.request_redraw();
}

#[cfg(test)]
pub(crate) fn composer_text_elements(&self) -> Vec<TextElement> {
self.composer.text_elements()
Expand Down Expand Up @@ -1212,10 +1227,13 @@ impl BottomPane {
queued: Vec<String>,
pending_steers: Vec<String>,
rejected_steers: Vec<String>,
flush_available: bool,
) {
self.pending_input_preview.pending_steers = pending_steers;
self.pending_input_preview.rejected_steers = rejected_steers;
self.pending_input_preview.queued_messages = queued;
self.pending_input_preview
.set_flush_available(flush_available);
self.request_redraw();
}

Expand Down Expand Up @@ -2422,6 +2440,7 @@ mod tests {
vec!["Queued follow-up question".to_string()],
Vec::new(),
Vec::new(),
/*flush_available*/ false,
);

let width = 48;
Expand Down Expand Up @@ -2453,6 +2472,7 @@ mod tests {
vec!["Queued follow-up question".to_string()],
Vec::new(),
Vec::new(),
/*flush_available*/ false,
);
pane.hide_status_indicator();

Expand Down Expand Up @@ -2485,6 +2505,7 @@ mod tests {
vec!["Queued follow-up question".to_string()],
Vec::new(),
Vec::new(),
/*flush_available*/ false,
);

let width = 48;
Expand Down
62 changes: 62 additions & 0 deletions codex-rs/tui/src/bottom_pane/pending_input_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ pub(crate) struct PendingInputPreview {
/// Key combination rendered in the hint line. Defaults to Alt+Up but may
/// be overridden for terminals where that chord is unavailable.
edit_binding: Option<key_hint::KeyBinding>,
/// Key combination rendered for merging every queued follow-up into the active turn.
/// Supplied by the resolved runtime keymap so a remapped binding is displayed.
flush_binding: Option<key_hint::KeyBinding>,
/// Whether a flush would actually do something right now. The flush only applies to a
/// running turn whose queue head is mergeable, so the hint stays hidden otherwise.
flush_available: bool,
/// Key combination rendered for immediately interrupting and sending steers.
interrupt_binding: Option<key_hint::KeyBinding>,
}
Expand All @@ -40,6 +46,8 @@ impl PendingInputPreview {
rejected_steers: Vec::new(),
queued_messages: Vec::new(),
edit_binding: Some(key_hint::alt(KeyCode::Up)),
flush_binding: None,
flush_available: false,
interrupt_binding: Some(key_hint::plain(KeyCode::Esc)),
}
}
Expand All @@ -51,6 +59,14 @@ impl PendingInputPreview {
self.edit_binding = binding;
}

pub(crate) fn set_flush_binding(&mut self, binding: Option<key_hint::KeyBinding>) {
self.flush_binding = binding;
}

pub(crate) fn set_flush_available(&mut self, available: bool) {
self.flush_available = available;
}

pub(crate) fn set_interrupt_binding(&mut self, binding: Option<key_hint::KeyBinding>) {
self.interrupt_binding = binding;
}
Expand Down Expand Up @@ -151,6 +167,19 @@ impl PendingInputPreview {
}
}

if self.flush_available
&& let Some(flush_binding) = self.flush_binding
{
lines.push(
Line::from(vec![
" ".into(),
flush_binding.into(),
" submit all now".into(),
])
.dim(),
);
}

if !self.queued_messages.is_empty()
&& let Some(edit_binding) = self.edit_binding
{
Expand Down Expand Up @@ -348,6 +377,39 @@ mod tests {
);
}

#[test]
fn render_queued_message_with_remapped_flush_binding() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Please continue.".to_string());
queue.set_flush_binding(Some(key_hint::plain(KeyCode::F(12))));
queue.set_flush_available(true);
let width = 48;
let height = queue.desired_height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
queue.render(Rect::new(0, 0, width, height), &mut buf);
assert_snapshot!(
"render_queued_message_with_remapped_flush_binding",
format!("{buf:?}")
);
}

#[test]
fn flush_hint_is_hidden_when_flush_is_unavailable() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Please continue.".to_string());
queue.set_flush_binding(Some(key_hint::shift(KeyCode::Enter)));
let width = 48;
let without_hint = queue.desired_height(width);

queue.set_flush_available(true);

assert_eq!(
queue.desired_height(width),
without_hint + 1,
"the flush hint should only add a row once a flush would do something"
);
}

#[test]
fn render_pending_steers_above_queued_messages() {
let mut queue = PendingInputPreview::new();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
source: tui/src/bottom_pane/pending_input_preview.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 48, height: 4 },
content: [
"• Queued follow-up inputs ",
" ↳ Please continue. ",
" f12 submit all now ",
" ⌥ + ↑ edit last queued message ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 20, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 22, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 34, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg;
use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus;
use codex_protocol::request_permissions::RequestPermissionsEvent;
use codex_protocol::user_input::ByteRange;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use codex_protocol::user_input::TextElement;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/chatwidget/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ impl ChatWidget {
widget
.bottom_pane
.set_queued_message_edit_binding(widget.queued_message_edit_hint_binding);
widget.bottom_pane.set_queued_message_flush_binding(
widget.chat_keymap.flush_queued_messages.first().copied(),
);
#[cfg(target_os = "windows")]
widget.bottom_pane.set_windows_degraded_sandbox_active(
crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
Expand Down
Loading
Loading