fix(tui): restore cancelled prompt cursor at end (#26457)

## Why

Pressing `Esc` on a turn that produced no visible output restores the
submitted prompt so the user can keep editing it. That restore path
preserved the prompt content, images, and mention bindings, but left the
composer cursor at the start of the restored text. The next edit
therefore inserted at the beginning instead of continuing from the end
of the prompt.

## What Changed

- Move the cursor to the end after
`BottomPane::set_composer_text_with_mention_bindings` rehydrates a
restored draft.
- Add test-only cursor accessors so restore tests can assert the
composer state directly.
- Extend the queued restore regression to assert the restored composer
cursor is positioned at `text.len()`.

## How to Test

Manual reviewer flow:

1. Start Codex in the TUI.
2. Submit a prompt that will take long enough to interrupt.
3. Press `Esc` before any visible assistant output appears.
4. Confirm the prompt is restored into the composer and the cursor is at
the end, so typing appends to the prompt.
5. Repeat with a prompt that includes an attached image or resolved
mention and confirm the restored content remains intact.

Targeted tests:

- `just test -p codex-tui
chatwidget::tests::composer_submission::queued_restore_with_remote_images_keeps_local_placeholder_mapping`

Lint note:

- `just argument-comment-lint` is blocked locally by the existing Bazel
`compiler-rt` empty glob failure before analyzing touched code. The
touched Rust diff was manually inspected and adds no new opaque
positional literal callsites.
This commit is contained in:
Felipe Coury
2026-06-05 15:10:13 -03:00
committed by GitHub
Unverified
parent 713192381b
commit 679a944dbc
3 changed files with 12 additions and 0 deletions
@@ -1258,6 +1258,11 @@ impl ChatComposer {
self.draft.textarea.cursor() + if self.draft.is_bash_mode { 1 } else { 0 }
}
#[cfg(test)]
pub(crate) fn cursor(&self) -> usize {
self.current_cursor()
}
fn history_navigation_cursor(&self) -> usize {
if self.draft.is_bash_mode && self.draft.textarea.cursor() == 0 {
0
+6
View File
@@ -786,6 +786,7 @@ impl BottomPane {
local_image_paths,
mention_bindings,
);
self.composer.move_cursor_to_end();
self.request_redraw();
}
@@ -824,6 +825,11 @@ impl BottomPane {
self.composer.current_text()
}
#[cfg(test)]
pub(crate) fn composer_cursor(&self) -> usize {
self.composer.cursor()
}
pub(crate) fn composer_draft_snapshot(&self) -> chat_composer::ComposerDraftSnapshot {
self.composer.draft_snapshot()
}
@@ -730,6 +730,7 @@ async fn queued_restore_with_remote_images_keeps_local_placeholder_mapping() {
});
assert_eq!(chat.bottom_pane.composer_text(), text);
assert_eq!(chat.bottom_pane.composer_cursor(), text.len());
assert_eq!(chat.bottom_pane.composer_text_elements(), text_elements);
assert_eq!(chat.bottom_pane.composer_local_images(), local_images);
assert_eq!(chat.remote_image_urls(), remote_image_urls);