On an empty composer, ↑ walks back through the thread's sent prompts and ↓ walks forward, past the newest back to an empty composer. Editing a recalled prompt ends it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Showing 1 changed file with 62 additions and 1 deletion.
| @@ -174,6 +174,9 @@ pub struct ChatView { | ||
| 174 | 174 | /// The thread folder's files, for the `@` menu, once read. |
| 175 | 175 | files: Option<(PathBuf, Vec<String>)>, |
| 176 | 176 | files_task: Task<()>, |
| 177 | + /// Where ↑/↓ are in the thread's sent prompts: the entry shown, and the | |
| 178 | + /// text put in the composer for it. | |
| 179 | + history: Option<(usize, String)>, | |
| 177 | 180 | /// Prompts put aside with Ctrl+S, oldest first. |
| 178 | 181 | stash: Vec<Stashed>, |
| 179 | 182 | stash_open: bool, |
| @@ -304,6 +307,7 @@ impl ChatView { | ||
| 304 | 307 | menu_dismissed: None, |
| 305 | 308 | files: None, |
| 306 | 309 | files_task: Task::ready(()), |
| 310 | + history: None, | |
| 307 | 311 | stash: Vec::new(), |
| 308 | 312 | stash_open: false, |
| 309 | 313 | stash_selected: 0, |
| @@ -554,6 +558,54 @@ impl ChatView { | ||
| 554 | 558 | } |
| 555 | 559 | } |
| 556 | 560 | |
| 561 | + /// The thread's sent prompts, oldest first, without repeats in a row. | |
| 562 | + fn prompt_history(&self, cx: &App) -> Vec<String> { | |
| 563 | + let mut prompts: Vec<String> = Vec::new(); | |
| 564 | + for item in &self.session.read(cx).items { | |
| 565 | + if let ChatItem::User { text, .. } = item | |
| 566 | + && !text.trim().is_empty() | |
| 567 | + && prompts.last().map(String::as_str) != Some(text.as_ref()) | |
| 568 | + { | |
| 569 | + prompts.push(text.to_string()); | |
| 570 | + } | |
| 571 | + } | |
| 572 | + prompts | |
| 573 | + } | |
| 574 | + | |
| 575 | + /// Terminal-style recall, as in T3 Code: ↑ on an empty composer walks | |
| 576 | + /// back through the thread's prompts, ↓ walks forward and past the | |
| 577 | + /// newest clears the composer again. Editing a recalled prompt ends it. | |
| 578 | + fn recall_prompt(&mut self, older: bool, window: &mut Window, cx: &mut Context<Self>) -> bool { | |
| 579 | + let text = self.composer.read(cx).value().to_string(); | |
| 580 | + let browsing = self.history.as_ref().filter(|(_, recalled)| *recalled == text).map(|(at, _)| *at); | |
| 581 | + if browsing.is_none() && (!text.is_empty() || !older) { | |
| 582 | + self.history = None; | |
| 583 | + return false; | |
| 584 | + } | |
| 585 | + let prompts = self.prompt_history(cx); | |
| 586 | + if prompts.is_empty() { | |
| 587 | + return false; | |
| 588 | + } | |
| 589 | + let next = match (browsing, older) { | |
| 590 | + (None, _) => Some(prompts.len() - 1), | |
| 591 | + (Some(at), true) => Some(at.saturating_sub(1)), | |
| 592 | + (Some(at), false) if at + 1 < prompts.len() => Some(at + 1), | |
| 593 | + (Some(_), false) => None, | |
| 594 | + }; | |
| 595 | + let value = match next { | |
| 596 | + Some(at) => { | |
| 597 | + self.history = Some((at, prompts[at].clone())); | |
| 598 | + prompts[at].clone() | |
| 599 | + } | |
| 600 | + None => { | |
| 601 | + self.history = None; | |
| 602 | + String::new() | |
| 603 | + } | |
| 604 | + }; | |
| 605 | + self.composer.update(cx, |composer, cx| composer.set_value(value, window, cx)); | |
| 606 | + true | |
| 607 | + } | |
| 608 | + | |
| 557 | 609 | /// Puts the composer's prompt aside (Ctrl+S), or with nothing to stash, |
| 558 | 610 | /// opens the stash. |
| 559 | 611 | fn stash_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) { |
| @@ -891,7 +943,16 @@ impl ChatView { | ||
| 891 | 943 | self.session.update(cx, |session, cx| session.toggle_plan(cx)); |
| 892 | 944 | return; |
| 893 | 945 | } |
| 894 | - let Some(menu) = self.command_menu(cx) else { return }; | |
| 946 | + let Some(menu) = self.command_menu(cx) else { | |
| 947 | + let plain = !(modifiers.control || modifiers.alt || modifiers.shift || modifiers.platform); | |
| 948 | + let asking = self.session.read(cx).pending_ask().is_some(); | |
| 949 | + if plain && !asking && matches!(keystroke.key.as_str(), "up" | "down") { | |
| 950 | + if self.recall_prompt(keystroke.key == "up", window, cx) { | |
| 951 | + cx.stop_propagation(); | |
| 952 | + } | |
| 953 | + } | |
| 954 | + return; | |
| 955 | + }; | |
| 895 | 956 | if modifiers.control || modifiers.alt || modifiers.shift { |
| 896 | 957 | return; |
| 897 | 958 | } |