tui: preserve remote image attachments across resume/backtrack (#10590)

## Summary
This PR makes app-server-provided image URLs first-class attachments in
TUI, so they survive resume/backtrack/history recall and are resubmitted
correctly.

<img width="715" height="491" alt="Screenshot 2026-02-12 at 8 27 08 PM"
src="https://github.com/user-attachments/assets/226cbd35-8f0c-4e51-a13e-459ef5dd1927"
/>

Can delete the attached image upon backtracking:
<img width="716" height="301" alt="Screenshot 2026-02-12 at 8 27 31 PM"
src="https://github.com/user-attachments/assets/4558d230-f1bd-4eed-a093-8e1ab9c6db27"
/>

In both history and composer, remote images are rendered as normal
`[Image #N]` placeholders, with numbering unified with local images.

## What changed
- Plumb remote image URLs through TUI message state:
  - `UserHistoryCell`
  - `BacktrackSelection`
  - `ChatComposerHistory::HistoryEntry`
  - `ChatWidget::UserMessage`
- Show remote images as placeholder rows inside the composer box (above
textarea), and in history cells.
- Support keyboard selection/deletion for remote image rows in composer
(`Up`/`Down`, `Delete`/`Backspace`).
- Preserve remote-image-only turns in local composer history (Up/Down
recall), including restore after backtrack.
- Ensure submit/queue/backtrack resubmit include remote images in model
input (`UserInput::Image`), and keep request shape stable for
remote-image-only turns.
- Keep image numbering contiguous across remote + local images:
  - remote images occupy `[Image #1]..[Image #M]`
  - local images start at `[Image #M+1]`
  - deletion renumbers consistently.
- In protocol conversion, increment shared image index for remote images
too, so mixed remote/local image tags stay in a single sequence.
- Simplify restore logic to trust in-memory attachment order (no
placeholder-number parsing path).
- Backtrack/replay rollback handling now queues trims through
`AppEvent::ApplyThreadRollback` and syncs transcript overlay/deferred
lines after trims, so overlay/transcript state stays consistent.
- Trim trailing blank rendered lines from user history rendering to
avoid oversized blank padding.

## Docs + tests
- Updated: `docs/tui-chat-composer.md` (remote image flow,
selection/deletion, numbering offsets)
- Added/updated tests across `tui/src/chatwidget/tests.rs`,
`tui/src/app.rs`, `tui/src/app_backtrack.rs`, `tui/src/history_cell.rs`,
and `tui/src/bottom_pane/chat_composer.rs`
- Added snapshot coverage for remote image composer states, including
deleting the first of two remote images.

## Validation
- `just fmt`
- `cargo test -p codex-tui`

## Codex author
`codex fork 019c2636-1571-74a1-8471-15a3b1c3f49d`
This commit is contained in:
Charley Cunningham
2026-02-13 14:54:06 -08:00
committed by GitHub
Unverified
parent 395729910c
commit 26a7cd21e2
15 changed files with 1713 additions and 92 deletions
+33 -13
View File
@@ -56,10 +56,11 @@ The solution is to detect paste-like _bursts_ and buffer them into a single expl
Up/Down recall is handled by `ChatComposerHistory` and merges two sources:
- **Persistent history** (cross-session, fetched from `~/.codex/history.jsonl`): text-only. It
does **not** carry text element ranges or local image attachments, so recalling one of these
entries only restores the text.
does **not** carry text element ranges or image attachments, so recalling one of these entries
only restores the text.
- **Local history** (current session): stores the full submission payload, including text
elements and local image paths. Recalling a local entry rehydrates placeholders and attachments.
elements, local image paths, and remote image URLs. Recalling a local entry rehydrates
placeholders and attachments.
This distinction keeps the on-disk history backward compatible and avoids persisting attachments,
while still providing a richer recall experience for in-session edits.
@@ -127,6 +128,23 @@ positional args, Enter auto-submits without calling `prepare_submission_text`. T
- Prunes attachments based on expanded placeholders.
- Clears pending pastes after a successful auto-submit.
## Remote image rows (selection/deletion flow)
Remote image URLs are shown as `[Image #N]` rows above the textarea, inside the same composer box.
They are attachment rows, not editable textarea content.
- TUI can remove these rows, but cannot type before/between them.
- Press `Up` at textarea cursor position `0` to select the last remote image row.
- While selected, `Up`/`Down` moves selection across remote image rows.
- Pressing `Down` on the last row exits remote-row selection and returns to textarea editing.
- `Delete` or `Backspace` removes the selected remote image row.
Image numbering is unified:
- Remote image rows always occupy `[Image #1]..[Image #M]`.
- Local attached image placeholders start after that offset (`[Image #M+1]..`).
- Removing remote rows relabels local placeholders so numbering stays contiguous.
## History navigation (Up/Down) and backtrack prefill
`ChatComposerHistory` merges two kinds of history:
@@ -139,6 +157,7 @@ Local history entries capture:
- raw text (including placeholders),
- `TextElement` ranges for placeholders,
- local image paths,
- remote image URLs,
- pending large-paste payloads (for drafts).
Persistent history entries only restore text. They intentionally do **not** rehydrate attachments
@@ -150,17 +169,17 @@ line). This keeps multiline cursor movement intact while preserving shell-like h
### Draft recovery (Ctrl+C)
Ctrl+C clears the composer but stashes the full draft state (text elements, image paths, and
pending paste payloads) into local history. Pressing Up immediately restores that draft, including
image placeholders and large-paste placeholders with their payloads.
Ctrl+C clears the composer but stashes the full draft state (text elements, local image paths,
remote image URLs, and pending paste payloads) into local history. Pressing Up immediately restores
that draft, including image placeholders and large-paste placeholders with their payloads.
### Submitted message recall
After a successful submission, the local history entry stores the submitted text and any element
ranges and local image paths. Pending paste payloads are cleared during submission, so large-paste
placeholders are expanded into their full text before being recorded. This means:
After a successful submission, the local history entry stores the submitted text, element ranges,
local image paths, and remote image URLs. Pending paste payloads are cleared during submission, so
large-paste placeholders are expanded into their full text before being recorded. This means:
- Up/Down recall of a submitted message restores image placeholders and their local paths.
- Up/Down recall of a submitted message restores remote image rows plus local image placeholders.
- Recalled entries place the cursor at end-of-line to match typical shell history editing.
- Large-paste placeholders are not expected in recalled submitted history; the text is the
expanded paste content.
@@ -168,14 +187,15 @@ placeholders are expanded into their full text before being recorded. This means
### Backtrack prefill
Backtrack selections read `UserHistoryCell` data from the transcript. The composer prefill now
reuses the selected messages text elements and local image paths, so image placeholders and
attachments rehydrate when rolling back to a prior user message.
reuses the selected messages text elements, local image paths, and remote image URLs, so image
placeholders and attachments rehydrate when rolling back to a prior user message.
### External editor edits
When the composer content is replaced from an external editor, the composer rebuilds text elements
and keeps only attachments whose placeholders still appear in the new text. Image placeholders are
then normalized to `[Image #1]..[Image #N]` to keep attachment mapping consistent after edits.
then normalized to `[Image #M]..[Image #N]`, where `M` starts after the number of remote image
rows, to keep attachment mapping consistent after edits.
## Paste burst: concepts and assumptions