TUI/Core: preserve duplicate skill/app mention selection across submit + resume (#10855)

## What changed

- In `codex-rs/core/src/skills/injection.rs`, we now honor explicit
`UserInput::Skill { name, path }` first, then fall back to text mentions
only when safe.
- In `codex-rs/tui/src/bottom_pane/chat_composer.rs`, mention selection
is now token-bound (selected mention is tied to the specific inserted
`$token`), and we snapshot bindings at submit time so selection is not
lost.
- In `codex-rs/tui/src/chatwidget.rs` and
`codex-rs/tui/src/bottom_pane/mod.rs`, submit/queue paths now consume
the submit-time mention snapshot (instead of rereading cleared composer
state).
- In `codex-rs/tui/src/mention_codec.rs` and
`codex-rs/tui/src/bottom_pane/chat_composer_history.rs`, history now
round-trips mention targets so resume restores the same selected
duplicate.
- In `codex-rs/tui/src/bottom_pane/skill_popup.rs` and
`codex-rs/tui/src/bottom_pane/chat_composer.rs`, duplicate labels are
normalized to `[Repo]` / `[App]`, app rows no longer show `Connected -`,
and description space is a bit wider.

<img width="550" height="163" alt="Screenshot 2026-02-05 at 9 56 56 PM"
src="https://github.com/user-attachments/assets/346a7eb2-a342-4a49-aec8-68dfec0c7d89"
/>
<img width="550" height="163" alt="Screenshot 2026-02-05 at 9 57 09 PM"
src="https://github.com/user-attachments/assets/5e04d9af-cccf-4932-98b3-c37183e445ed"
/>


## Before vs now

- Before: selecting a duplicate could still submit the default/repo
match, and resume could lose which duplicate was originally selected.
- Now: the exact selected target (skill path or app id) is preserved
through submit, queue/restore, and resume.

## Manual test

1. Build and run this branch locally:
   - `cd /Users/daniels/code/codex/codex-rs`
   - `cargo build -p codex-cli --bin codex`
   - `./target/debug/codex`
2. Open mention picker with `$` and pick a duplicate entry (not the
first one).
3. Confirm duplicate UI:
   - repo duplicate rows show `[Repo]`
   - app duplicate rows show `[App]`
   - app description does **not** start with `Connected -`
4. Submit the prompt, then press Up to restore draft and submit again.  
   Expected: it keeps the same selected duplicate target.
5. Use `/resume` to reopen the session and send again.  
Expected: restored mention still resolves to the same duplicate target.
This commit is contained in:
daniel-oai
2026-02-06 15:59:00 -08:00
committed by GitHub
parent daeef06bec
commit 84bce2b8e6
15 changed files with 865 additions and 116 deletions

View File

@@ -13,7 +13,6 @@
//!
//! Some UI is time-based rather than input-based, such as the transient "press again to quit"
//! hint. The pane schedules redraws so those hints can expire even when the UI is otherwise idle.
use std::collections::HashMap;
use std::path::PathBuf;
use crate::app_event::ConnectorsSnapshot;
@@ -55,6 +54,14 @@ pub(crate) struct LocalImageAttachment {
pub(crate) placeholder: String,
pub(crate) path: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct MentionBinding {
/// Mention token text without the leading `$`.
pub(crate) mention: String,
/// Canonical mention target (for example `app://...` or absolute SKILL.md path).
pub(crate) path: String,
}
mod chat_composer;
mod chat_composer_history;
mod command_popup;
@@ -228,14 +235,19 @@ impl BottomPane {
self.request_redraw();
}
pub fn take_mention_paths(&mut self) -> HashMap<String, String> {
self.composer.take_mention_paths()
pub fn take_mention_bindings(&mut self) -> Vec<MentionBinding> {
self.composer.take_mention_bindings()
}
/// Clear pending attachments and mention paths e.g. when a slash command doesn't submit text.
pub fn take_recent_submission_mention_bindings(&mut self) -> Vec<MentionBinding> {
self.composer.take_recent_submission_mention_bindings()
}
/// Clear pending attachments and mention bindings e.g. when a slash command doesn't submit text.
pub(crate) fn drain_pending_submission_state(&mut self) {
let _ = self.take_recent_submission_images_with_placeholders();
let _ = self.take_mention_paths();
let _ = self.take_recent_submission_mention_bindings();
let _ = self.take_mention_bindings();
}
pub fn set_steer_enabled(&mut self, enabled: bool) {
@@ -419,7 +431,7 @@ impl BottomPane {
///
/// This is intended for fresh input where mention linkage does not need to
/// survive; it routes to `ChatComposer::set_text_content`, which resets
/// `mention_paths`.
/// mention bindings.
pub(crate) fn set_composer_text(
&mut self,
text: String,
@@ -437,18 +449,18 @@ impl BottomPane {
/// Use this when rehydrating a draft after a local validation/gating
/// failure (for example unsupported image submit) so previously selected
/// mention targets remain stable across retry.
pub(crate) fn set_composer_text_with_mention_paths(
pub(crate) fn set_composer_text_with_mention_bindings(
&mut self,
text: String,
text_elements: Vec<TextElement>,
local_image_paths: Vec<PathBuf>,
mention_paths: HashMap<String, String>,
mention_bindings: Vec<MentionBinding>,
) {
self.composer.set_text_content_with_mention_paths(
self.composer.set_text_content_with_mention_bindings(
text,
text_elements,
local_image_paths,
mention_paths,
mention_bindings,
);
self.request_redraw();
}
@@ -481,6 +493,10 @@ impl BottomPane {
self.composer.local_images()
}
pub(crate) fn composer_mention_bindings(&self) -> Vec<MentionBinding> {
self.composer.mention_bindings()
}
#[cfg(test)]
pub(crate) fn composer_local_image_paths(&self) -> Vec<PathBuf> {
self.composer.local_image_paths()