fix(tui): persist ctrl-c draft via app event (#21397)

## Why

The main branch started failing after #21351 merged because the merge
commit kept calling `AppCommand::add_to_history` from
`BottomPane::clear_composer_for_ctrl_c`, but main had already removed
that helper as part of the history persistence refactor. The PR head
passed because it was based on an older main commit where the helper
still existed.

This restores the Ctrl+C draft-stashing behavior using the current
app-event path instead of the removed command helper.

## What Changed

- Store the active `ThreadId` in `BottomPane` when history metadata is
provided.
- Emit `AppEvent::AppendMessageHistoryEntry` for Ctrl+C-cleared drafts.
- Update the slash-clear regression test to assert the current history
event shape.

## How to Test

Targeted tests:
- `cargo test -p codex-tui
slash_clear_after_ctrl_c_keeps_stashed_draft_recallable`

Broader local checks:
- `just fix -p codex-tui`
- `just argument-comment-lint -p codex-tui`
- `git diff --check origin/main...HEAD`
- `cargo test -p codex-tui` reached completion; the fixed test passed,
and the only local failures were
`status::tests::status_permissions_full_disk_managed_*`, blocked by this
machine config rejecting `DangerFullAccess` via
`/etc/codex/requirements.toml`.
This commit is contained in:
Felipe Coury
2026-05-06 16:03:11 -03:00
committed by GitHub
parent f32c496144
commit 6b7d6cafa0
2 changed files with 18 additions and 7 deletions

View File

@@ -17,7 +17,6 @@ use std::collections::VecDeque;
use std::path::PathBuf;
use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::app_command::AppCommand;
use crate::app_event::AppEvent;
use crate::app_event::ConnectorsSnapshot;
use crate::app_event_sender::AppEventSender;
@@ -209,6 +208,7 @@ pub(crate) struct BottomPane {
app_event_tx: AppEventSender,
frame_requester: FrameRequester,
thread_id: Option<ThreadId>,
has_input_focus: bool,
enhanced_keys_supported: bool,
@@ -274,6 +274,7 @@ impl BottomPane {
last_composer_activity_at: None,
app_event_tx,
frame_requester,
thread_id: None,
has_input_focus,
enhanced_keys_supported,
disable_paste_burst,
@@ -780,8 +781,14 @@ impl BottomPane {
pub(crate) fn clear_composer_for_ctrl_c(&mut self) {
if let Some(text) = self.composer.clear_for_ctrl_c() {
self.app_event_tx
.send(AppEvent::CodexOp(AppCommand::add_to_history(text)));
if let Some(thread_id) = self.thread_id {
self.app_event_tx
.send(AppEvent::AppendMessageHistoryEntry { thread_id, text });
} else {
tracing::warn!(
"failed to append Ctrl+C-cleared draft to history: no active thread id"
);
}
}
self.request_redraw();
}
@@ -1438,6 +1445,7 @@ impl BottomPane {
log_id: u64,
entry_count: usize,
) {
self.thread_id = Some(thread_id);
self.composer
.set_history_metadata(thread_id, log_id, entry_count);
}

View File

@@ -1584,7 +1584,13 @@ async fn slash_clear_requests_ui_clear_when_idle() {
#[tokio::test]
async fn slash_clear_after_ctrl_c_keeps_stashed_draft_recallable() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let thread_id = ThreadId::new();
chat.thread_id = Some(thread_id);
chat.bottom_pane
.set_history_metadata(thread_id, /*log_id*/ 1, /*entry_count*/ 0);
submit_composer_text(&mut chat, "ok");
assert_eq!(next_add_to_history_event(&mut rx), "ok");
let stashed_draft = "explain why history recall lost this draft";
@@ -1592,10 +1598,7 @@ async fn slash_clear_after_ctrl_c_keeps_stashed_draft_recallable() {
.set_composer_text(stashed_draft.to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert_eq!(chat.bottom_pane.composer_text(), "");
assert_matches!(
rx.try_recv(),
Ok(AppEvent::CodexOp(Op::AddToHistory { text })) if text == stashed_draft
);
assert_eq!(next_add_to_history_event(&mut rx), stashed_draft);
chat.bottom_pane
.set_composer_text("/clear".to_string(), Vec::new(), Vec::new());