mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Preserve TUI question state and integrate history and queue navigation (#42903)
## What changed - Retain question drafts, selections, expanded state, and handled message IDs across thread input restoration and reconnects. Preserve buffered live questions during session refresh without replaying their message text. - Enable history recall and search in question answers, honoring updated key bindings and stripping image attachments and placeholders from recalled entries. Preserve Vim mode when an answer is not accepted. - Let forward navigation past the last question restore the latest queued message as an editable main composer draft. Keep working status and queued messages visible alongside questions. - Show a countdown for newly arrived collapsed questions and clear it when the question editor is opened or used. - Disable question submission and skipping while the thread is unavailable. ## Testing Add regression tests and snapshots for draft restoration and replay deduplication, history search and key bindings, queued-message editing, countdown rendering, and question layout with working status. GitOrigin-RevId: de016120a979db860fd0d4b8098b322b312b7d84
This commit is contained in:
@@ -247,6 +247,10 @@ impl ChatWidget {
|
||||
|
||||
/// Rebuild and update the bottom-pane pending-input preview.
|
||||
pub(super) fn refresh_pending_input_preview(&mut self) {
|
||||
let has_queued = self.has_queued_follow_up_messages();
|
||||
if let Some(questions) = &mut self.bottom_pane.questions {
|
||||
questions.has_queued_messages = has_queued;
|
||||
}
|
||||
let preview = self.input_queue.preview();
|
||||
self.bottom_pane.set_pending_input_preview(
|
||||
preview.queued_messages,
|
||||
|
||||
@@ -98,6 +98,11 @@ impl ChatWidget {
|
||||
pending_draft: &mut Option<ComposerDraftSnapshot>,
|
||||
) {
|
||||
if self.has_active_view()
|
||||
|| self
|
||||
.bottom_pane
|
||||
.questions
|
||||
.as_ref()
|
||||
.is_some_and(|q| q.expanded)
|
||||
|| self.has_pending_protected_request()
|
||||
|| !self.bottom_pane.composer_input_enabled()
|
||||
{
|
||||
@@ -432,7 +437,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn capture_thread_input_state(&self) -> Option<ThreadInputState> {
|
||||
pub(crate) fn capture_thread_input_state(&mut self) -> Option<ThreadInputState> {
|
||||
let draft = self.bottom_pane.composer_draft_snapshot();
|
||||
let composer = ThreadComposerState {
|
||||
text: draft.text,
|
||||
@@ -443,6 +448,11 @@ impl ChatWidget {
|
||||
pending_pastes: draft.pending_pastes,
|
||||
};
|
||||
Some(ThreadInputState {
|
||||
questions: self
|
||||
.bottom_pane
|
||||
.questions
|
||||
.as_deref_mut()
|
||||
.map(crate::bottom_pane::AsyncQuestions::capture),
|
||||
composer: composer.has_content().then_some(composer),
|
||||
safety_buffering_prompt: self.safety_buffering_prompt.clone(),
|
||||
pending_steers: self.input_queue.pending_steers.clone(),
|
||||
@@ -474,6 +484,7 @@ impl ChatWidget {
|
||||
let restored_task_running =
|
||||
preserve_in_flight_turn && input_state.as_ref().is_some_and(|state| state.task_running);
|
||||
if let Some(input_state) = input_state {
|
||||
self.bottom_pane.restore_questions(input_state.questions);
|
||||
self.input_queue.recovered_queue = input_state.recovered_queue;
|
||||
self.current_collaboration_mode = input_state.current_collaboration_mode;
|
||||
self.active_collaboration_mask = input_state.active_collaboration_mask;
|
||||
|
||||
@@ -440,6 +440,16 @@ impl ChatWidget {
|
||||
notification: ItemCompletedNotification,
|
||||
replay_kind: Option<ReplayKind>,
|
||||
) {
|
||||
// Buffered live notifications can introduce questions; historical turn replay cannot.
|
||||
if replay_kind == Some(ReplayKind::ThreadSnapshot)
|
||||
&& let ThreadItem::AgentMessage {
|
||||
id,
|
||||
questions: Some(questions),
|
||||
..
|
||||
} = ¬ification.item
|
||||
{
|
||||
self.add_async_questions(id, questions);
|
||||
}
|
||||
match notification.item {
|
||||
item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_completed(item),
|
||||
item => self.handle_thread_item(
|
||||
|
||||
@@ -12,6 +12,7 @@ impl ChatWidget {
|
||||
questions: &[AsyncUserInputQuestion],
|
||||
) {
|
||||
self.bottom_pane.push_async_questions(message_id, questions);
|
||||
self.refresh_pending_input_preview();
|
||||
}
|
||||
|
||||
pub(super) fn handle_question_key(&mut self, key: KeyEvent) -> bool {
|
||||
@@ -49,7 +50,6 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
} else if forward
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
&& let Some(questions) = self
|
||||
@@ -62,6 +62,19 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
return true;
|
||||
}
|
||||
if expanded
|
||||
&& forward
|
||||
&& !self.blocks_direct_input
|
||||
&& let Some(composer) = self.pop_latest_queued_composer_state()
|
||||
{
|
||||
if let Some(questions) = &mut self.bottom_pane.questions {
|
||||
questions.set_expanded(/*expanded*/ false);
|
||||
}
|
||||
self.restore_composer_state(composer);
|
||||
self.refresh_pending_input_preview();
|
||||
self.request_redraw();
|
||||
return true;
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
if !expanded {
|
||||
|
||||
@@ -54,6 +54,9 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn pause_unavailable_thread(&mut self) {
|
||||
if let Some(questions) = &mut self.bottom_pane.questions {
|
||||
questions.delivery_enabled = false;
|
||||
}
|
||||
self.turn_lifecycle
|
||||
.restore_running(/*running*/ false, Instant::now());
|
||||
self.update_task_running_state();
|
||||
|
||||
@@ -1756,6 +1756,7 @@ async fn restore_thread_input_state_applies_running_state_policy() {
|
||||
text_elements: Vec::new(),
|
||||
});
|
||||
let input_state = ThreadInputState {
|
||||
questions: None,
|
||||
composer: Some(ThreadComposerState {
|
||||
text: "composer draft".to_string(),
|
||||
..Default::default()
|
||||
@@ -2441,8 +2442,12 @@ async fn reconnect_holds_only_recovered_input_until_manually_edited() {
|
||||
chat.set_queue_autosend_suppressed(/*suppressed*/ false);
|
||||
if let Some(text) = recovered {
|
||||
assert!(!chat.maybe_send_next_queued_input());
|
||||
assert_eq!(chat.pop_latest_queued_composer_state().unwrap().text, text);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::ALT));
|
||||
assert_eq!(chat.bottom_pane.composer_text(), text);
|
||||
assert_no_submit_op(&mut ops);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(next_submit_op(&mut ops), Op::UserTurn { .. });
|
||||
chat.input_queue.user_turn_pending_start = false;
|
||||
}
|
||||
chat.input_queue
|
||||
.queued_user_messages
|
||||
|
||||
@@ -136,6 +136,12 @@ async fn question_inputs_support_vim_editing_and_search() {
|
||||
}
|
||||
assert!(chat.bottom_pane.questions.as_ref().unwrap().expanded);
|
||||
let normal = render_bottom_popup(&chat, /*width*/ 80);
|
||||
let editor = chat.bottom_pane.questions.as_mut().unwrap();
|
||||
editor.delivery_enabled = false;
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_eq!(render_bottom_popup(&chat, /*width*/ 80), normal);
|
||||
let editor = chat.bottom_pane.questions.as_mut().unwrap();
|
||||
editor.delivery_enabled = true;
|
||||
for ch in "dw".chars() {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch)));
|
||||
}
|
||||
@@ -169,6 +175,101 @@ async fn question_inputs_support_vim_editing_and_search() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn question_history_search_uses_updated_bindings() {
|
||||
for (binding, word) in [("f12", "earlier"), ("ctrl-x r", "persistent")] {
|
||||
let (mut chat, _rx, mut 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*/ 7, /*entry_count*/ 1);
|
||||
chat.bottom_pane.record_replayed_user_message_history(
|
||||
crate::bottom_pane::HistoryEntry::with_pending_and_remote(
|
||||
"[Image #2]earlier answer".into(),
|
||||
vec![TextElement::new((0..10).into(), Some("[Image #2]".into()))],
|
||||
vec![PathBuf::from("/tmp/image.png")],
|
||||
Vec::new(),
|
||||
vec!["https://example.com/remote.png".into()],
|
||||
),
|
||||
);
|
||||
chat.add_async_questions("message", &questions());
|
||||
let config = toml::from_str(&format!(
|
||||
"[composer]\nhistory_search_previous = '{binding}'"
|
||||
))
|
||||
.unwrap();
|
||||
let keymap = RuntimeKeymap::from_config(&config).unwrap();
|
||||
chat.apply_keymap_update(config, &keymap);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::ALT));
|
||||
chat.bottom_pane.record_replayed_user_message_history(
|
||||
crate::bottom_pane::HistoryEntry::new("newest answer".into()),
|
||||
);
|
||||
for (key, expected) in [
|
||||
(KeyCode::Up, "newest answer"),
|
||||
(KeyCode::Up, "earlier answer"),
|
||||
(KeyCode::Down, "newest answer"),
|
||||
] {
|
||||
chat.handle_key_event(KeyEvent::from(key));
|
||||
let rendered = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
rendered.contains(expected),
|
||||
"{key:?}: expected {expected}, got {rendered}"
|
||||
);
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
|
||||
// Chords reach the editor using the dispatch binding resolved by the app.
|
||||
let (key, modifiers) = keymap.composer.history_search_previous[0].parts();
|
||||
chat.handle_key_event(KeyEvent::new(key, modifiers));
|
||||
for ch in word.chars() {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch)));
|
||||
}
|
||||
if binding != "f12" {
|
||||
chat.bottom_pane.on_history_lookup_response(
|
||||
crate::app_event::HistoryLookupResponse::Entry {
|
||||
offset: 0,
|
||||
log_id: 7,
|
||||
entry: Some("persistent answer".into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert!(op_rx.try_recv().is_err());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_answer(
|
||||
op_rx.try_recv().unwrap(),
|
||||
&format!("> Which way?\n\n{word} answer"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn question_editor_keeps_working_status_and_queued_messages_visible() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.show_welcome_banner = false;
|
||||
chat.bottom_pane.set_task_running(/*running*/ true);
|
||||
chat.bottom_pane
|
||||
.set_composer_text("main draft".into(), Vec::new(), Vec::new());
|
||||
chat.input_queue
|
||||
.queued_user_messages
|
||||
.push_back(UserMessage::from("queued follow-up".to_string()).into());
|
||||
chat.refresh_pending_input_preview();
|
||||
chat.add_async_questions("message", &questions());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::ALT));
|
||||
let open = render_bottom_popup(&chat, /*width*/ 80);
|
||||
let open = open.split("\n 1 of 2").next().unwrap().trim_end();
|
||||
chat.bottom_pane.set_task_running(/*running*/ false);
|
||||
assert!(chat.bottom_pane.questions.as_ref().unwrap().expanded);
|
||||
chat.input_queue.queued_user_messages.clear();
|
||||
chat.refresh_pending_input_preview();
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
|
||||
chat.bottom_pane.set_status_line_enabled(/*enabled*/ false);
|
||||
let completed = render_bottom_popup(&chat, /*width*/ 80);
|
||||
let completed = completed.split("\n\n›").next().unwrap();
|
||||
insta::assert_snapshot!(
|
||||
"questions_with_status_and_queue",
|
||||
format!("OPEN\n{open}\n\nONLY QUESTIONS\n{completed}")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_questions_reject_blank_answers_but_allow_explicit_skip() {
|
||||
for options in [None, Some(vec!["Named".to_string()])] {
|
||||
@@ -274,6 +375,94 @@ async fn question_key_repeats_do_not_consume_another_question() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn question_queue_pop_becomes_an_ordinary_composer_draft() {
|
||||
let (mut chat, _rx, mut ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
for text in ["older", "newer"] {
|
||||
chat.input_queue
|
||||
.queued_user_messages
|
||||
.push_back(UserMessage::from(text).into());
|
||||
}
|
||||
open_questions(&mut chat, /*options*/ None);
|
||||
let forward = KeyEvent::new(KeyCode::Up, KeyModifiers::ALT);
|
||||
chat.handle_key_event(forward);
|
||||
insta::assert_snapshot!(
|
||||
"last_question_with_queue",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
);
|
||||
chat.handle_key_event(forward);
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "newer");
|
||||
chat.bottom_pane.handle_paste(" edited".into());
|
||||
chat.handle_key_event(forward);
|
||||
assert!(chat.bottom_pane.questions.as_ref().unwrap().expanded);
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "newer edited");
|
||||
chat.handle_key_event(forward);
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "older");
|
||||
assert!(chat.input_queue.queued_user_messages.is_empty());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_answer(ops.try_recv().unwrap(), "older");
|
||||
chat.handle_key_event(forward);
|
||||
assert!(chat.bottom_pane.questions.as_ref().unwrap().expanded);
|
||||
assert!(
|
||||
!chat
|
||||
.bottom_pane
|
||||
.questions
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.has_queued_messages
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn question_drafts_survive_navigation_and_snapshot_replay() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.bottom_pane
|
||||
.set_composer_text("main draft".into(), Vec::new(), Vec::new());
|
||||
open_questions(&mut chat, /*options*/ None);
|
||||
chat.bottom_pane.handle_paste("first draft".into());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::ALT));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('2')));
|
||||
chat.bottom_pane.handle_paste("second draft".into());
|
||||
let saved = chat.capture_thread_input_state();
|
||||
let (mut chat, _rx, mut ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let item = AppServerThreadItem::AgentMessage {
|
||||
id: "buffered".into(),
|
||||
text: String::new(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: Some(vec![question("Buffered?", /*options*/ None)]),
|
||||
};
|
||||
for kind in [
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
ReplayKind::ThreadSnapshot,
|
||||
] {
|
||||
chat.replay_thread_item(item.clone(), "turn".into(), kind);
|
||||
assert!(chat.bottom_pane.questions.is_none());
|
||||
}
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
item,
|
||||
thread_id: "thread".into(),
|
||||
turn_id: "turn".into(),
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
Some(ReplayKind::ThreadSnapshot),
|
||||
);
|
||||
assert_eq!(question_count(&chat), 1);
|
||||
chat.restore_reconnected_input(saved);
|
||||
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("second draft"));
|
||||
chat.pause_unavailable_thread();
|
||||
chat.handle_question_key(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::CONTROL));
|
||||
chat.handle_question_key(KeyEvent::from(KeyCode::Enter));
|
||||
assert_eq!(question_count(&chat), 3);
|
||||
assert!(ops.try_recv().is_err());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::ALT));
|
||||
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("first draft"));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::ALT));
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "main draft");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn single_question_spacing_with_working_status() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -287,7 +476,7 @@ async fn single_question_spacing_with_working_status() {
|
||||
.iter()
|
||||
.position(|line| line.trim() == "Only question?")
|
||||
.unwrap();
|
||||
assert!(rows[question - 2].contains("Working"));
|
||||
assert!(rows[question - 2].contains("Queued follow-up inputs"));
|
||||
assert!(rows[question - 1].is_empty());
|
||||
assert_eq!(rows[question + 2].trim(), "A typed answer");
|
||||
assert!(rows[question + 3].is_empty());
|
||||
|
||||
@@ -376,6 +376,7 @@ async fn restore_thread_input_state_restores_pending_steers_without_downgrading_
|
||||
|
||||
chat.restore_thread_input_state(
|
||||
Some(ThreadInputState {
|
||||
questions: None,
|
||||
composer: None,
|
||||
safety_buffering_prompt: None,
|
||||
pending_steers: VecDeque::from([expected_pending.clone()]),
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
• Queued follow-up inputs
|
||||
|
||||
1 of 2
|
||||
|
||||
Which way?
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
• Queued follow-up inputs
|
||||
↳ older
|
||||
↳ newer
|
||||
|
||||
2 of 2
|
||||
Second?
|
||||
|
||||
› 1. Next
|
||||
2. Other
|
||||
|
||||
enter submit ctrl + ] skip ⌥ + ↓ prev question ⌥ + ↑ queued messages
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
• Queued follow-up inputs
|
||||
|
||||
What next?
|
||||
|
||||
› 1.
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
• Queued follow-up inputs
|
||||
|
||||
What next?
|
||||
|
||||
Type your answer
|
||||
|
||||
-1
@@ -4,7 +4,6 @@ expression: "render_bottom_popup(&chat, 100)"
|
||||
---
|
||||
• Queued follow-up inputs
|
||||
↳ queued
|
||||
shift + ← edit last queued message
|
||||
|
||||
1 of 2
|
||||
|
||||
|
||||
-1
@@ -4,7 +4,6 @@ expression: "render_bottom_popup(&chat, 100)"
|
||||
---
|
||||
• Queued follow-up inputs
|
||||
↳ queued
|
||||
⌥ + ↑ edit last queued message
|
||||
|
||||
1 of 4
|
||||
|
||||
|
||||
+4
@@ -3,6 +3,8 @@ source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "format!(\"NORMAL\\n{normal}\\n\\nSEARCH\\n{search}\")"
|
||||
---
|
||||
NORMAL
|
||||
• Queued follow-up inputs
|
||||
|
||||
Which way?
|
||||
|
||||
1. Named
|
||||
@@ -12,6 +14,8 @@ NORMAL
|
||||
enter submit ctrl + ] skip ⌥ + ↓ main prompt
|
||||
|
||||
SEARCH
|
||||
• Queued follow-up inputs
|
||||
|
||||
Which way?
|
||||
|
||||
1. Named
|
||||
|
||||
+4
@@ -3,6 +3,8 @@ source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "format!(\"NORMAL\\n{normal}\\n\\nSEARCH\\n{search}\")"
|
||||
---
|
||||
NORMAL
|
||||
• Queued follow-up inputs
|
||||
|
||||
Which way?
|
||||
|
||||
alpha beta
|
||||
@@ -11,6 +13,8 @@ NORMAL
|
||||
enter submit ctrl + ] skip ⌥ + ↓ main prompt
|
||||
|
||||
SEARCH
|
||||
• Queued follow-up inputs
|
||||
|
||||
Which way?
|
||||
|
||||
alpha beta
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/questions_tests.rs
|
||||
expression: "format!(\"OPEN\\n{open}\\n\\nONLY QUESTIONS\\n{completed}\")"
|
||||
---
|
||||
OPEN
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Queued follow-up inputs
|
||||
↳ queued follow-up
|
||||
|
||||
ONLY QUESTIONS
|
||||
• Queued follow-up inputs
|
||||
? 2 questions
|
||||
⌥ + ↑ to answer
|
||||
+2
@@ -4,6 +4,8 @@ expression: rendered
|
||||
---
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Queued follow-up inputs
|
||||
|
||||
Only question?
|
||||
|
||||
A typed answer
|
||||
|
||||
@@ -122,6 +122,7 @@ impl ThreadComposerState {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ThreadInputState {
|
||||
pub(crate) questions: Option<crate::bottom_pane::QuestionState>,
|
||||
pub(super) composer: Option<ThreadComposerState>,
|
||||
pub(super) safety_buffering_prompt: Option<UserMessage>,
|
||||
pub(crate) pending_steers: VecDeque<PendingSteer>,
|
||||
|
||||
Reference in New Issue
Block a user