mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
tui: clarify pending steer follow-ups (#13841)
## Summary - split the pending input preview into labeled pending-steer and queued follow-up sections - explain that pending steers submit after the next tool call and that Esc can interrupt and send them immediately - treat Esc as an interrupt-plus-resubmit path when pending steers exist, with updated TUI snapshots and tests Queues and steers: <img width="1038" height="263" alt="Screenshot 2026-03-07 at 10 17 17 PM" src="https://github.com/user-attachments/assets/4ef433ef-27a3-4b7c-ad69-2046f6eb89e6" /> After pressing Esc: <img width="1046" height="320" alt="Screenshot 2026-03-07 at 10 17 21 PM" src="https://github.com/user-attachments/assets/0f4d89e0-b6b9-486a-9f04-b6021f169ba7" /> ## Codex author `codex resume 019cc6f4-2cca-7803-b717-8264526dbd97` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
f41b1638c9
commit
4ad3b59de3
@@ -12,11 +12,13 @@ use crate::wrapping::adaptive_wrap_lines;
|
||||
|
||||
/// Widget that displays pending steers plus user messages queued while a turn is in progress.
|
||||
///
|
||||
/// The widget shows pending steers first, then queued user messages. It only
|
||||
/// shows the edit hint at the bottom (e.g. "⌥ + ↑ edit") when there are actual
|
||||
/// queued user messages to pop back into the composer. Because some terminals
|
||||
/// intercept certain modifier-key combinations, the displayed binding is
|
||||
/// configurable via [`set_edit_binding`](Self::set_edit_binding).
|
||||
/// The widget renders pending steers first, then queued user messages, as two
|
||||
/// labeled sections. Pending steers explain that they will be submitted after
|
||||
/// the next tool/result boundary unless the user presses Esc to interrupt and
|
||||
/// send them immediately. The edit hint at the bottom only appears when there
|
||||
/// are actual queued user messages to pop back into the composer. Because some
|
||||
/// terminals intercept certain modifier-key combinations, the displayed
|
||||
/// binding is configurable via [`set_edit_binding`](Self::set_edit_binding).
|
||||
pub(crate) struct PendingInputPreview {
|
||||
pub pending_steers: Vec<String>,
|
||||
pub queued_messages: Vec<String>,
|
||||
@@ -55,6 +57,15 @@ impl PendingInputPreview {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_section_header(lines: &mut Vec<Line<'static>>, width: u16, header: Line<'static>) {
|
||||
let mut spans = vec!["• ".dim()];
|
||||
spans.extend(header.spans);
|
||||
lines.extend(adaptive_wrap_lines(
|
||||
std::iter::once(Line::from(spans)),
|
||||
RtOptions::new(width as usize).subsequent_indent(Line::from(" ".dim())),
|
||||
));
|
||||
}
|
||||
|
||||
fn as_renderable(&self, width: u16) -> Box<dyn Renderable> {
|
||||
if (self.pending_steers.is_empty() && self.queued_messages.is_empty()) || width < 4 {
|
||||
return Box::new(());
|
||||
@@ -62,28 +73,48 @@ impl PendingInputPreview {
|
||||
|
||||
let mut lines = vec![];
|
||||
|
||||
for steer in &self.pending_steers {
|
||||
let wrapped = adaptive_wrap_lines(
|
||||
steer.lines().map(|line| Line::from(line.dim())),
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(" ! pending steer: ".dim()))
|
||||
.subsequent_indent(Line::from(" ")),
|
||||
if !self.pending_steers.is_empty() {
|
||||
Self::push_section_header(
|
||||
&mut lines,
|
||||
width,
|
||||
Line::from(vec![
|
||||
"Messages to be submitted after next tool call".into(),
|
||||
" (press ".dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to interrupt and send immediately)".dim(),
|
||||
]),
|
||||
);
|
||||
Self::push_truncated_preview_lines(&mut lines, wrapped, Line::from(" …".dim()));
|
||||
|
||||
for steer in &self.pending_steers {
|
||||
let wrapped = adaptive_wrap_lines(
|
||||
steer.lines().map(|line| Line::from(line.dim())),
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(" ↳ ".dim()))
|
||||
.subsequent_indent(Line::from(" ")),
|
||||
);
|
||||
Self::push_truncated_preview_lines(&mut lines, wrapped, Line::from(" …".dim()));
|
||||
}
|
||||
}
|
||||
|
||||
for message in &self.queued_messages {
|
||||
let wrapped = adaptive_wrap_lines(
|
||||
message.lines().map(|line| Line::from(line.dim().italic())),
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(" ↳ ".dim()))
|
||||
.subsequent_indent(Line::from(" ")),
|
||||
);
|
||||
Self::push_truncated_preview_lines(
|
||||
&mut lines,
|
||||
wrapped,
|
||||
Line::from(" …".dim().italic()),
|
||||
);
|
||||
if !self.queued_messages.is_empty() {
|
||||
if !lines.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
Self::push_section_header(&mut lines, width, "Queued follow-up messages".into());
|
||||
|
||||
for message in &self.queued_messages {
|
||||
let wrapped = adaptive_wrap_lines(
|
||||
message.lines().map(|line| Line::from(line.dim().italic())),
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(" ↳ ".dim()))
|
||||
.subsequent_indent(Line::from(" ")),
|
||||
);
|
||||
Self::push_truncated_preview_lines(
|
||||
&mut lines,
|
||||
wrapped,
|
||||
Line::from(" …".dim().italic()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.queued_messages.is_empty() {
|
||||
@@ -91,7 +122,7 @@ impl PendingInputPreview {
|
||||
Line::from(vec![
|
||||
" ".into(),
|
||||
self.edit_binding.into(),
|
||||
" edit".into(),
|
||||
" edit last queued message".into(),
|
||||
])
|
||||
.dim(),
|
||||
);
|
||||
@@ -131,7 +162,7 @@ mod tests {
|
||||
fn desired_height_one_message() {
|
||||
let mut queue = PendingInputPreview::new();
|
||||
queue.queued_messages.push("Hello, world!".to_string());
|
||||
assert_eq!(queue.desired_height(40), 2);
|
||||
assert_eq!(queue.desired_height(40), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -219,8 +250,8 @@ mod tests {
|
||||
let width = 36;
|
||||
let height = queue.desired_height(width);
|
||||
assert_eq!(
|
||||
height, 2,
|
||||
"expected one message row plus hint row for URL-like token"
|
||||
height, 3,
|
||||
"expected header, one message row, and hint row for URL-like token"
|
||||
);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
|
||||
+13
-10
@@ -1,27 +1,30 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/queued_user_messages.rs
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 5 },
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 6 },
|
||||
content: [
|
||||
"• Queued follow-up messages ",
|
||||
" ↳ This is ",
|
||||
" a message ",
|
||||
" with many ",
|
||||
" … ",
|
||||
" ⌥ + ↑ edit ",
|
||||
" ⌥ + ↑ edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 11, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 11, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 13, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 5, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 13, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 5, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 34, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+11
-8
@@ -1,30 +1,33 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/queued_user_messages.rs
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 5 },
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 6 },
|
||||
content: [
|
||||
"• Queued follow-up messages ",
|
||||
" ↳ Hello, world! ",
|
||||
" ↳ This is another message ",
|
||||
" ↳ This is a third message ",
|
||||
" ↳ This is a fourth message ",
|
||||
" ⌥ + ↑ edit ",
|
||||
" ⌥ + ↑ edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 27, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 27, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 28, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 28, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 34, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+14
-8
@@ -3,21 +3,27 @@ source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 4 },
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 6 },
|
||||
content: [
|
||||
" ! pending steer: First line ",
|
||||
"• Messages to be submitted after next tool call ",
|
||||
" (press esc to interrupt and send immediately) ",
|
||||
" ↳ First line ",
|
||||
" Second line ",
|
||||
" Third line ",
|
||||
" … ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 15, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 2, 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: 47, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 5, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 15, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 5, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,18 +1,21 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/queued_user_messages.rs
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 2 },
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 3 },
|
||||
content: [
|
||||
"• Queued follow-up messages ",
|
||||
" ↳ Hello, world! ",
|
||||
" ⌥ + ↑ edit ",
|
||||
" ⌥ + ↑ edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 34, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,15 +1,20 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
assertion_line: 237
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 1 },
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 3 },
|
||||
content: [
|
||||
" ! pending steer: Please continue. ",
|
||||
"• Messages to be submitted after next tool call ",
|
||||
" (press esc to interrupt and send immediately) ",
|
||||
" ↳ Please continue. ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 35, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 47, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 20, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+19
-10
@@ -1,25 +1,34 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
assertion_line: 252
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 52, height: 4 },
|
||||
area: Rect { x: 0, y: 0, width: 52, height: 8 },
|
||||
content: [
|
||||
" ! pending steer: Please continue. ",
|
||||
" ! pending steer: Check the last command output. ",
|
||||
"• Messages to be submitted after next tool call ",
|
||||
" (press esc to interrupt and send immediately) ",
|
||||
" ↳ Please continue. ",
|
||||
" ↳ Check the last command output. ",
|
||||
" ",
|
||||
"• Queued follow-up messages ",
|
||||
" ↳ Queued follow-up question ",
|
||||
" ⌥ + ↑ edit ",
|
||||
" ⌥ + ↑ edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 35, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 49, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 47, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 29, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 20, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 34, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 29, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 34, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+10
-7
@@ -1,22 +1,25 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/queued_user_messages.rs
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 3 },
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 4 },
|
||||
content: [
|
||||
"• Queued follow-up messages ",
|
||||
" ↳ Hello, world! ",
|
||||
" ↳ This is another message ",
|
||||
" ⌥ + ↑ edit ",
|
||||
" ⌥ + ↑ edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 27, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 27, 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,
|
||||
]
|
||||
}
|
||||
|
||||
+12
-9
@@ -1,25 +1,28 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/queued_user_messages.rs
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 4 },
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 5 },
|
||||
content: [
|
||||
"• Queued follow-up messages ",
|
||||
" ↳ This is a longer message that should",
|
||||
" be wrapped ",
|
||||
" ↳ This is another message ",
|
||||
" ⌥ + ↑ edit ",
|
||||
" ⌥ + ↑ edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
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: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 27, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 14, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 14, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 27, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 34, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+2
-1
@@ -2,8 +2,9 @@
|
||||
source: tui/src/bottom_pane/mod.rs
|
||||
expression: "render_snapshot(&pane, area)"
|
||||
---
|
||||
• Queued follow-up messages
|
||||
↳ Queued follow-up question
|
||||
⌥ + ↑ edit
|
||||
⌥ + ↑ edit last queued message
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
|
||||
+2
-1
@@ -4,8 +4,9 @@ expression: "render_snapshot(&pane, area)"
|
||||
---
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Queued follow-up messages
|
||||
↳ Queued follow-up question
|
||||
⌥ + ↑ edit
|
||||
⌥ + ↑ edit last queued message
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
|
||||
+2
-1
@@ -6,8 +6,9 @@ expression: "render_snapshot(&pane, area)"
|
||||
└ First detail line
|
||||
Second detail line
|
||||
|
||||
• Queued follow-up messages
|
||||
↳ Queued follow-up question
|
||||
⌥ + ↑ edit
|
||||
⌥ + ↑ edit last queued message
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
|
||||
@@ -630,6 +630,9 @@ pub(crate) struct ChatWidget {
|
||||
// The bottom pane shows these above queued drafts until core records the
|
||||
// corresponding user message item.
|
||||
pending_steers: VecDeque<PendingSteer>,
|
||||
// When set, the next interrupt should resubmit all pending steers as one
|
||||
// fresh user turn instead of restoring them into the composer.
|
||||
submit_pending_steers_after_interrupt: bool,
|
||||
/// Terminal-appropriate keybinding for popping the most-recently queued
|
||||
/// message back into the composer. Determined once at construction time via
|
||||
/// [`queued_message_edit_binding_for_terminal`] and propagated to
|
||||
@@ -922,6 +925,45 @@ fn remap_placeholders_for_message(message: UserMessage, next_label: &mut usize)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_user_messages(messages: Vec<UserMessage>) -> UserMessage {
|
||||
let mut combined = UserMessage {
|
||||
text: String::new(),
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
};
|
||||
let total_remote_images = messages
|
||||
.iter()
|
||||
.map(|message| message.remote_image_urls.len())
|
||||
.sum::<usize>();
|
||||
let mut next_image_label = total_remote_images + 1;
|
||||
|
||||
for (idx, message) in messages.into_iter().enumerate() {
|
||||
if idx > 0 {
|
||||
combined.text.push('\n');
|
||||
}
|
||||
let UserMessage {
|
||||
text,
|
||||
text_elements,
|
||||
local_images,
|
||||
remote_image_urls,
|
||||
mention_bindings,
|
||||
} = remap_placeholders_for_message(message, &mut next_image_label);
|
||||
append_text_with_rebased_elements(
|
||||
&mut combined.text,
|
||||
&mut combined.text_elements,
|
||||
&text,
|
||||
text_elements,
|
||||
);
|
||||
combined.local_images.extend(local_images);
|
||||
combined.remote_image_urls.extend(remote_image_urls);
|
||||
combined.mention_bindings.extend(mention_bindings);
|
||||
}
|
||||
|
||||
combined
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ReplayKind {
|
||||
ResumeInitialMessages,
|
||||
@@ -1521,6 +1563,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_task_complete(&mut self, last_agent_message: Option<String>, from_replay: bool) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
if let Some(message) = last_agent_message.as_ref()
|
||||
&& !message.trim().is_empty()
|
||||
{
|
||||
@@ -1874,6 +1917,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_server_overloaded_error(&mut self, message: String) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
self.finalize_turn();
|
||||
|
||||
let message = if message.trim().is_empty() {
|
||||
@@ -1888,6 +1932,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
self.finalize_turn();
|
||||
self.add_to_history(history_cell::new_error_event(message));
|
||||
self.request_redraw();
|
||||
@@ -1978,15 +2023,35 @@ impl ChatWidget {
|
||||
if reason == TurnAbortReason::Interrupted {
|
||||
self.clear_unified_exec_processes();
|
||||
}
|
||||
let send_pending_steers_immediately = self.submit_pending_steers_after_interrupt;
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
if reason != TurnAbortReason::ReviewEnded {
|
||||
self.add_to_history(history_cell::new_error_event(
|
||||
"Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.".to_owned(),
|
||||
));
|
||||
if send_pending_steers_immediately {
|
||||
self.add_to_history(history_cell::new_info_event(
|
||||
"Model interrupted to submit steer instructions.".to_owned(),
|
||||
None,
|
||||
));
|
||||
} else {
|
||||
self.add_to_history(history_cell::new_error_event(
|
||||
"Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Core clears pending_input before emitting TurnAborted, so any unacknowledged steers
|
||||
// still tracked here must be restored locally instead of waiting for a later commit.
|
||||
if let Some(combined) = self.drain_pending_messages_for_restore() {
|
||||
if send_pending_steers_immediately {
|
||||
let pending_steers: Vec<UserMessage> = self
|
||||
.pending_steers
|
||||
.drain(..)
|
||||
.map(|pending| pending.user_message)
|
||||
.collect();
|
||||
if !pending_steers.is_empty() {
|
||||
self.submit_user_message(merge_user_messages(pending_steers));
|
||||
} else if let Some(combined) = self.drain_pending_messages_for_restore() {
|
||||
self.restore_user_message_to_composer(combined);
|
||||
}
|
||||
} else if let Some(combined) = self.drain_pending_messages_for_restore() {
|
||||
self.restore_user_message_to_composer(combined);
|
||||
}
|
||||
self.refresh_pending_input_preview();
|
||||
@@ -2027,42 +2092,7 @@ impl ChatWidget {
|
||||
to_merge.push(existing_message);
|
||||
}
|
||||
|
||||
let mut combined = UserMessage {
|
||||
text: String::new(),
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
};
|
||||
let total_remote_images = to_merge
|
||||
.iter()
|
||||
.map(|message| message.remote_image_urls.len())
|
||||
.sum::<usize>();
|
||||
let mut next_image_label = total_remote_images + 1;
|
||||
|
||||
for (idx, message) in to_merge.into_iter().enumerate() {
|
||||
if idx > 0 {
|
||||
combined.text.push('\n');
|
||||
}
|
||||
let UserMessage {
|
||||
text,
|
||||
text_elements,
|
||||
local_images,
|
||||
remote_image_urls,
|
||||
mention_bindings,
|
||||
} = remap_placeholders_for_message(message, &mut next_image_label);
|
||||
append_text_with_rebased_elements(
|
||||
&mut combined.text,
|
||||
&mut combined.text_elements,
|
||||
&text,
|
||||
text_elements,
|
||||
);
|
||||
combined.local_images.extend(local_images);
|
||||
combined.remote_image_urls.extend(remote_image_urls);
|
||||
combined.mention_bindings.extend(mention_bindings);
|
||||
}
|
||||
|
||||
Some(combined)
|
||||
Some(merge_user_messages(to_merge))
|
||||
}
|
||||
|
||||
fn restore_user_message_to_composer(&mut self, user_message: UserMessage) {
|
||||
@@ -3161,6 +3191,7 @@ impl ChatWidget {
|
||||
forked_from: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
pending_steers: VecDeque::new(),
|
||||
submit_pending_steers_after_interrupt: false,
|
||||
queued_message_edit_binding,
|
||||
show_welcome_banner: is_first_run,
|
||||
startup_tooltip_override,
|
||||
@@ -3347,6 +3378,7 @@ impl ChatWidget {
|
||||
plan_item_active: false,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
pending_steers: VecDeque::new(),
|
||||
submit_pending_steers_after_interrupt: false,
|
||||
queued_message_edit_binding,
|
||||
show_welcome_banner: is_first_run,
|
||||
startup_tooltip_override,
|
||||
@@ -3517,6 +3549,7 @@ impl ChatWidget {
|
||||
forked_from: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
pending_steers: VecDeque::new(),
|
||||
submit_pending_steers_after_interrupt: false,
|
||||
queued_message_edit_binding,
|
||||
show_welcome_banner: false,
|
||||
startup_tooltip_override: None,
|
||||
@@ -3656,6 +3689,19 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(key_event.code, KeyCode::Esc)
|
||||
&& matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
&& !self.pending_steers.is_empty()
|
||||
&& self.bottom_pane.is_task_running()
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
{
|
||||
self.submit_pending_steers_after_interrupt = true;
|
||||
if !self.submit_op(Op::Interrupt) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
code: KeyCode::BackTab,
|
||||
@@ -4798,6 +4844,7 @@ impl ChatWidget {
|
||||
self.on_interrupted_turn(ev.reason);
|
||||
}
|
||||
TurnAbortReason::Replaced => {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
self.pending_steers.clear();
|
||||
self.refresh_pending_input_preview();
|
||||
self.on_error("Turn aborted: replaced by a new task".to_owned())
|
||||
|
||||
+1
-1
@@ -5,6 +5,7 @@ expression: term.backend().vt100().screen().contents()
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Queued follow-up messages
|
||||
↳ Hello, world! 0
|
||||
↳ Hello, world! 1
|
||||
↳ Hello, world! 2
|
||||
@@ -21,7 +22,6 @@ expression: term.backend().vt100().screen().contents()
|
||||
↳ Hello, world! 13
|
||||
↳ Hello, world! 14
|
||||
↳ Hello, world! 15
|
||||
↳ Hello, world! 16
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: info
|
||||
---
|
||||
• Model interrupted to submit steer instructions.
|
||||
+2
-2
@@ -11,11 +11,11 @@ expression: term.backend().vt100().screen().contents()
|
||||
|
||||
|
||||
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Queued follow-up messages
|
||||
↳ Queued while /review is running.
|
||||
⌥ + ↑ edit
|
||||
⌥ + ↑ edit last queued message
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
|
||||
@@ -1866,6 +1866,7 @@ async fn make_chatwidget_manual(
|
||||
startup_tooltip_override: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
pending_steers: VecDeque::new(),
|
||||
submit_pending_steers_after_interrupt: false,
|
||||
queued_message_edit_binding: crate::key_hint::alt(KeyCode::Up),
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
@@ -1913,6 +1914,17 @@ fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) -> Op {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_interrupt_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) {
|
||||
loop {
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::Interrupt) => return,
|
||||
Ok(_) => continue,
|
||||
Err(TryRecvError::Empty) => panic!("expected interrupt op but queue was empty"),
|
||||
Err(TryRecvError::Disconnected) => panic!("expected interrupt op but channel closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_no_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) {
|
||||
while let Ok(op) = op_rx.try_recv() {
|
||||
assert!(
|
||||
@@ -4378,6 +4390,107 @@ async fn manual_interrupt_restores_pending_steers_to_composer() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn esc_interrupt_sends_all_pending_steers_immediately_and_keeps_existing_draft() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
chat.on_agent_message_delta("Final answer line\n".to_string());
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("first pending steer".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "first pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("second pending steer".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "second pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("queued draft".to_string()));
|
||||
chat.refresh_pending_input_preview();
|
||||
chat.bottom_pane
|
||||
.set_composer_text("still editing".to_string(), Vec::new(), Vec::new());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
next_interrupt_op(&mut op_rx);
|
||||
|
||||
chat.on_interrupted_turn(TurnAbortReason::Interrupted);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "first pending steer\nsecond pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected merged pending steers to submit, got {other:?}"),
|
||||
}
|
||||
|
||||
assert!(chat.pending_steers.is_empty());
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "still editing");
|
||||
assert_eq!(chat.queued_user_messages.len(), 1);
|
||||
assert_eq!(
|
||||
chat.queued_user_messages.front().unwrap().text,
|
||||
"queued draft"
|
||||
);
|
||||
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
inserted
|
||||
.iter()
|
||||
.any(|cell| lines_to_single_string(cell).contains("first pending steer"))
|
||||
);
|
||||
assert!(
|
||||
inserted
|
||||
.iter()
|
||||
.any(|cell| lines_to_single_string(cell).contains("second pending steer"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn esc_with_pending_steers_overrides_agent_command_interrupt_behavior() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("pending steer".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { .. } => {}
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/agent ".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
next_interrupt_op(&mut op_rx);
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "/agent ");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_interrupt_restores_pending_steer_mention_bindings_to_composer() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
@@ -6249,6 +6362,42 @@ async fn interrupted_turn_error_message_snapshot() {
|
||||
assert_snapshot!("interrupted_turn_error_message", last);
|
||||
}
|
||||
|
||||
// Snapshot test: interrupting specifically to submit pending steers shows an
|
||||
// informational banner instead of the generic "tell the model what to do
|
||||
// differently" error prompt.
|
||||
#[tokio::test]
|
||||
async fn interrupted_turn_pending_steers_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.pending_steers.push_back(pending_steer("steer 1"));
|
||||
chat.submit_pending_steers_after_interrupt = true;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let info = cells
|
||||
.iter()
|
||||
.map(|cell| lines_to_single_string(cell))
|
||||
.find(|line| line.contains("Model interrupted to submit steer instructions."))
|
||||
.expect("expected steer interrupt info message to be inserted");
|
||||
assert_snapshot!("interrupted_turn_pending_steers_message", info);
|
||||
}
|
||||
|
||||
/// Opening custom prompt from the review popup, pressing Esc returns to the
|
||||
/// parent popup, pressing Esc again dismisses all panels (back to normal mode).
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user