mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Merge 88bd2b750e into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -379,6 +379,50 @@ async fn test_get_has_changes_ignores_repo_fsmonitor_config() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_get_has_changes_ignores_configured_hooks_path() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let repo_path = create_test_git_repo(&temp_dir).await;
|
||||
let hooks_dir = repo_path.join(".git/hooks-path-test");
|
||||
let hook_path = hooks_dir.join("post-index-change");
|
||||
let marker_path = repo_path.join("hook-ran");
|
||||
|
||||
fs::create_dir_all(&hooks_dir).expect("create hook dir");
|
||||
fs::write(
|
||||
&hook_path,
|
||||
format!(
|
||||
"#!/bin/sh\nprintf ran > \"{}\"\n",
|
||||
marker_path.to_string_lossy()
|
||||
),
|
||||
)
|
||||
.expect("write post-index-change hook");
|
||||
let mut permissions = fs::metadata(&hook_path)
|
||||
.expect("read hook metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&hook_path, permissions).expect("mark hook executable");
|
||||
|
||||
Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"core.hooksPath",
|
||||
hooks_dir.to_string_lossy().as_ref(),
|
||||
])
|
||||
.current_dir(&repo_path)
|
||||
.output()
|
||||
.await
|
||||
.expect("configure hooks path");
|
||||
|
||||
fs::write(repo_path.join("test.txt"), "test content").expect("refresh tracked file");
|
||||
|
||||
assert_eq!(get_has_changes(&repo_path).await, Some(false));
|
||||
assert!(
|
||||
!marker_path.exists(),
|
||||
"metadata collection should not invoke configured hook directories"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_git_working_tree_state_clean_repo() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
|
||||
@@ -584,6 +584,51 @@ mod tests {
|
||||
assert_eq!(git_stdout(&root, &["ls-files"]), "MEMORY.md\n");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn write_index_ignores_configured_hooks_path() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let home = TempDir::new().expect("tempdir");
|
||||
let root = home.path().join("repo");
|
||||
let hooks_dir = root.join(".git/hooks-path-test");
|
||||
let marker_path = root.join("hook-ran");
|
||||
let hook_path = hooks_dir.join("post-index-change");
|
||||
|
||||
fs::create_dir_all(&root).expect("create root");
|
||||
fs::write(root.join("MEMORY.md"), "baseline").expect("write memory");
|
||||
reset_git_repository(&root).await.expect("reset repo");
|
||||
fs::create_dir_all(&hooks_dir).expect("create hook dir");
|
||||
fs::write(
|
||||
&hook_path,
|
||||
format!(
|
||||
"#!/bin/sh\nprintf ran > \"{}\"\n",
|
||||
marker_path.to_string_lossy()
|
||||
),
|
||||
)
|
||||
.expect("write post-index-change hook");
|
||||
let mut permissions = fs::metadata(&hook_path)
|
||||
.expect("read hook metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&hook_path, permissions).expect("mark hook executable");
|
||||
git_stdout(
|
||||
&root,
|
||||
&[
|
||||
"config",
|
||||
"core.hooksPath",
|
||||
hooks_dir.to_string_lossy().as_ref(),
|
||||
],
|
||||
);
|
||||
|
||||
write_index_from_head(&root).expect("rewrite baseline index");
|
||||
|
||||
assert!(
|
||||
!marker_path.exists(),
|
||||
"baseline index writes should not invoke configured hook directories"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diff_reports_added_modified_and_deleted_files() {
|
||||
let home = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -40,6 +40,7 @@ pub fn get_git_repo_root(base_dir: &Path) -> Option<PathBuf> {
|
||||
|
||||
/// Timeout for git commands to prevent freezing on large repositories
|
||||
const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5);
|
||||
const DISABLED_HOOKS_PATH: &str = if cfg!(windows) { "NUL" } else { "/dev/null" };
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
||||
pub struct GitInfo {
|
||||
@@ -375,6 +376,8 @@ async fn run_git_command_with_timeout(args: &[&str], cwd: &Path) -> Option<std::
|
||||
let mut command = Command::new("git");
|
||||
command
|
||||
.env("GIT_OPTIONAL_LOCKS", "0")
|
||||
// Keep internal Git helper commands independent of configured hook directories.
|
||||
.args(["-c", &format!("core.hooksPath={DISABLED_HOOKS_PATH}")])
|
||||
.args(["-c", "core.fsmonitor=false"])
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::process::Command;
|
||||
|
||||
use crate::GitToolingError;
|
||||
|
||||
const DISABLED_HOOKS_PATH: &str = if cfg!(windows) { "NUL" } else { "/dev/null" };
|
||||
|
||||
pub(crate) fn ensure_git_repository(path: &Path) -> Result<(), GitToolingError> {
|
||||
match run_git_for_stdout(
|
||||
path,
|
||||
@@ -98,7 +100,12 @@ where
|
||||
{
|
||||
let iterator = args.into_iter();
|
||||
let (lower, upper) = iterator.size_hint();
|
||||
let mut args_vec = Vec::with_capacity(upper.unwrap_or(lower));
|
||||
let mut args_vec = Vec::with_capacity(upper.unwrap_or(lower) + 2);
|
||||
// Keep internal Git helper commands independent of configured hook directories.
|
||||
args_vec.push(OsString::from("-c"));
|
||||
args_vec.push(OsString::from(format!(
|
||||
"core.hooksPath={DISABLED_HOOKS_PATH}"
|
||||
)));
|
||||
for arg in iterator {
|
||||
args_vec.push(OsString::from(arg.as_ref()));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
//! Editable composer draft state kept separate from composer control flow.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::bottom_pane::MentionBinding;
|
||||
use crate::bottom_pane::paste_burst::PasteBurst;
|
||||
use crate::bottom_pane::textarea::TextArea;
|
||||
use crate::bottom_pane::textarea::TextAreaState;
|
||||
|
||||
pub(super) struct DraftState {
|
||||
pub(super) textarea: TextArea,
|
||||
pub(super) textarea_state: RefCell<TextAreaState>,
|
||||
pub(super) is_bash_mode: bool,
|
||||
pub(super) pending_pastes: Vec<(String, String)>,
|
||||
pub(super) input_enabled: bool,
|
||||
pub(super) input_disabled_placeholder: Option<String>,
|
||||
pub(super) paste_burst: PasteBurst,
|
||||
pub(super) disable_paste_burst: bool,
|
||||
pub(super) mention_bindings: HashMap<u64, ComposerMentionBinding>,
|
||||
pub(super) recent_submission_mention_bindings: Vec<MentionBinding>,
|
||||
}
|
||||
|
||||
impl DraftState {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
textarea: TextArea::new(),
|
||||
textarea_state: RefCell::new(TextAreaState::default()),
|
||||
is_bash_mode: false,
|
||||
pending_pastes: Vec::new(),
|
||||
input_enabled: true,
|
||||
input_disabled_placeholder: None,
|
||||
paste_burst: PasteBurst::default(),
|
||||
disable_paste_burst: false,
|
||||
mention_bindings: HashMap::new(),
|
||||
recent_submission_mention_bindings: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ComposerMentionBinding {
|
||||
pub(super) mention: String,
|
||||
pub(super) path: String,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Footer and status-row presentation state for the chat composer.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use ratatui::text::Line;
|
||||
|
||||
use crate::bottom_pane::footer::CollaborationModeIndicator;
|
||||
use crate::bottom_pane::footer::FooterMode;
|
||||
use crate::bottom_pane::footer::GoalStatusIndicator;
|
||||
use crate::key_hint::KeyBinding;
|
||||
#[cfg(test)]
|
||||
use std::time::Duration;
|
||||
|
||||
pub(super) struct FooterState {
|
||||
pub(super) quit_shortcut_expires_at: Option<Instant>,
|
||||
pub(super) quit_shortcut_key: KeyBinding,
|
||||
pub(super) esc_backtrack_hint: bool,
|
||||
pub(super) use_shift_enter_hint: bool,
|
||||
pub(super) mode: FooterMode,
|
||||
pub(super) hint_override: Option<Vec<(String, String)>>,
|
||||
pub(super) plan_mode_nudge_visible: bool,
|
||||
pub(super) flash: Option<FooterFlash>,
|
||||
pub(super) context_window_percent: Option<i64>,
|
||||
pub(super) context_window_used_tokens: Option<i64>,
|
||||
pub(super) collaboration_mode_indicator: Option<CollaborationModeIndicator>,
|
||||
pub(super) goal_status_indicator: Option<GoalStatusIndicator>,
|
||||
pub(super) ide_context_active: bool,
|
||||
pub(super) status_line_value: Option<Line<'static>>,
|
||||
pub(super) status_line_hyperlink_url: Option<String>,
|
||||
pub(super) status_line_enabled: bool,
|
||||
pub(super) side_conversation_context_label: Option<String>,
|
||||
pub(super) active_agent_label: Option<String>,
|
||||
pub(super) external_editor_key: Option<KeyBinding>,
|
||||
pub(super) show_transcript_key: Option<KeyBinding>,
|
||||
pub(super) insert_newline_key: Option<KeyBinding>,
|
||||
pub(super) queue_key: Option<KeyBinding>,
|
||||
pub(super) toggle_shortcuts_key: Option<KeyBinding>,
|
||||
pub(super) history_search_key: Option<KeyBinding>,
|
||||
pub(super) reasoning_down_key: Option<KeyBinding>,
|
||||
pub(super) reasoning_up_key: Option<KeyBinding>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct FooterFlash {
|
||||
pub(super) line: Line<'static>,
|
||||
pub(super) expires_at: Instant,
|
||||
}
|
||||
|
||||
impl FooterState {
|
||||
pub(super) fn flash_visible(&self) -> bool {
|
||||
self.flash
|
||||
.as_ref()
|
||||
.is_some_and(|flash| Instant::now() < flash.expires_at)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn show_flash(&mut self, line: Line<'static>, duration: Duration) {
|
||||
let expires_at = Instant::now()
|
||||
.checked_add(duration)
|
||||
.unwrap_or_else(Instant::now);
|
||||
self.flash = Some(FooterFlash { line, expires_at });
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn status_line_text(&self) -> Option<String> {
|
||||
self.status_line_value.as_ref().map(|line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.map(|span| span.content.as_ref())
|
||||
.collect::<String>()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -102,10 +102,10 @@ impl ChatComposer {
|
||||
/// from replacing an empty composer with the latest prompt before the user has searched for
|
||||
/// anything.
|
||||
pub(super) fn begin_history_search(&mut self) -> (InputResult, bool) {
|
||||
if let Some(pasted) = self.paste_burst.flush_before_modified_input() {
|
||||
if let Some(pasted) = self.draft.paste_burst.flush_before_modified_input() {
|
||||
self.handle_paste(pasted);
|
||||
}
|
||||
self.paste_burst.clear_window_after_non_char();
|
||||
self.draft.paste_burst.clear_window_after_non_char();
|
||||
|
||||
if self.popups.current_file_query.is_some() {
|
||||
self.app_event_tx
|
||||
@@ -185,7 +185,7 @@ impl ChatComposer {
|
||||
{
|
||||
self.history_search = None;
|
||||
self.history.reset_search();
|
||||
self.footer_mode = reset_mode_after_activity(self.footer_mode);
|
||||
self.footer.mode = reset_mode_after_activity(self.footer.mode);
|
||||
self.move_cursor_to_end();
|
||||
}
|
||||
(InputResult::None, true)
|
||||
@@ -296,7 +296,7 @@ impl ChatComposer {
|
||||
return false;
|
||||
};
|
||||
self.history.reset_navigation();
|
||||
self.footer_mode = reset_mode_after_activity(self.footer_mode);
|
||||
self.footer.mode = reset_mode_after_activity(self.footer.mode);
|
||||
self.restore_draft(search.original_draft);
|
||||
true
|
||||
}
|
||||
@@ -385,7 +385,7 @@ impl ChatComposer {
|
||||
if !matches!(search.status, HistorySearchStatus::Match) || search.query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
Self::case_insensitive_match_ranges(self.textarea.text(), &search.query)
|
||||
Self::case_insensitive_match_ranges(self.draft.textarea.text(), &search.query)
|
||||
}
|
||||
|
||||
fn case_insensitive_match_ranges(text: &str, query: &str) -> Vec<Range<usize>> {
|
||||
@@ -520,7 +520,7 @@ mod tests {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
|
||||
assert!(composer.history_search_active());
|
||||
assert!(composer.textarea.is_empty());
|
||||
assert!(composer.draft.textarea.is_empty());
|
||||
assert_eq!(composer.footer_mode(), FooterMode::HistorySearch);
|
||||
}
|
||||
|
||||
@@ -558,18 +558,21 @@ mod tests {
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
assert!(composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "draft");
|
||||
assert_eq!(composer.draft.textarea.text(), "draft");
|
||||
|
||||
for ch in ['g', 'i', 't'] {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
}
|
||||
assert_eq!(composer.textarea.text(), "git status");
|
||||
assert_eq!(composer.draft.textarea.text(), "git status");
|
||||
assert_eq!(composer.footer_mode(), FooterMode::HistorySearch);
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert!(!composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "git status");
|
||||
assert_eq!(composer.textarea.cursor(), composer.textarea.text().len());
|
||||
assert_eq!(composer.draft.textarea.text(), "git status");
|
||||
assert_eq!(
|
||||
composer.draft.textarea.cursor(),
|
||||
composer.draft.textarea.text().len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -593,8 +596,8 @@ mod tests {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
}
|
||||
|
||||
assert_eq!(composer.textarea.text(), "git status");
|
||||
assert_eq!(composer.textarea.cursor(), "git status".len() - 1);
|
||||
assert_eq!(composer.draft.textarea.text(), "git status");
|
||||
assert_eq!(composer.draft.textarea.cursor(), "git status".len() - 1);
|
||||
assert_eq!(composer.footer_mode(), FooterMode::HistorySearch);
|
||||
}
|
||||
|
||||
@@ -618,13 +621,19 @@ mod tests {
|
||||
for ch in ['b', 'u', 'g'] {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
}
|
||||
assert_eq!(composer.textarea.text(), "Find and fix a bug in @filename");
|
||||
assert_eq!(
|
||||
composer.draft.textarea.text(),
|
||||
"Find and fix a bug in @filename"
|
||||
);
|
||||
|
||||
for _ in 0..3 {
|
||||
let _ =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
}
|
||||
assert_eq!(composer.textarea.text(), "Find and fix a bug in @filename");
|
||||
assert_eq!(
|
||||
composer.draft.textarea.text(),
|
||||
"Find and fix a bug in @filename"
|
||||
);
|
||||
assert!(
|
||||
composer
|
||||
.history_search
|
||||
@@ -635,7 +644,10 @@ mod tests {
|
||||
for _ in 0..3 {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||
}
|
||||
assert_eq!(composer.textarea.text(), "Find and fix a bug in @filename");
|
||||
assert_eq!(
|
||||
composer.draft.textarea.text(),
|
||||
"Find and fix a bug in @filename"
|
||||
);
|
||||
assert!(
|
||||
composer
|
||||
.history_search
|
||||
@@ -776,17 +788,17 @@ mod tests {
|
||||
.history
|
||||
.record_local_submission(HistoryEntry::new("remembered command".to_string()));
|
||||
composer.set_text_content("draft".to_string(), Vec::new(), Vec::new());
|
||||
composer.textarea.set_cursor(/*pos*/ 2);
|
||||
composer.draft.textarea.set_cursor(/*pos*/ 2);
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
assert_eq!(composer.textarea.text(), "draft");
|
||||
assert_eq!(composer.draft.textarea.text(), "draft");
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
|
||||
assert_eq!(composer.textarea.text(), "remembered command");
|
||||
assert_eq!(composer.draft.textarea.text(), "remembered command");
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(!composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "draft");
|
||||
assert_eq!(composer.textarea.cursor(), 2);
|
||||
assert_eq!(composer.draft.textarea.text(), "draft");
|
||||
assert_eq!(composer.draft.textarea.cursor(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -805,13 +817,13 @@ mod tests {
|
||||
.history
|
||||
.record_local_submission(HistoryEntry::new("remembered command".to_string()));
|
||||
composer.set_text_content("draft".to_string(), Vec::new(), Vec::new());
|
||||
composer.textarea.set_cursor(/*pos*/ 2);
|
||||
composer.draft.textarea.set_cursor(/*pos*/ 2);
|
||||
|
||||
let _ =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
let _ =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
|
||||
assert_eq!(composer.textarea.text(), "remembered command");
|
||||
assert_eq!(composer.draft.textarea.text(), "remembered command");
|
||||
composer
|
||||
}
|
||||
|
||||
@@ -824,8 +836,8 @@ mod tests {
|
||||
let _ = composer.handle_key_event(cancel_key);
|
||||
|
||||
assert!(!composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "draft");
|
||||
assert_eq!(composer.textarea.cursor(), 2);
|
||||
assert_eq!(composer.draft.textarea.text(), "draft");
|
||||
assert_eq!(composer.draft.textarea.cursor(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,18 +855,18 @@ mod tests {
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE));
|
||||
assert!(composer.is_in_paste_burst());
|
||||
assert_eq!(composer.textarea.text(), "");
|
||||
assert_eq!(composer.draft.textarea.text(), "");
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
|
||||
assert!(composer.history_search_active());
|
||||
assert!(!composer.is_in_paste_burst());
|
||||
assert_eq!(composer.textarea.text(), "h");
|
||||
assert_eq!(composer.draft.textarea.text(), "h");
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert!(!composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "h");
|
||||
assert_eq!(composer.draft.textarea.text(), "h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -881,18 +893,18 @@ mod tests {
|
||||
now += Duration::from_millis(1);
|
||||
}
|
||||
assert!(composer.is_in_paste_burst());
|
||||
assert_eq!(composer.textarea.text(), "");
|
||||
assert_eq!(composer.draft.textarea.text(), "");
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
|
||||
assert!(composer.history_search_active());
|
||||
assert!(!composer.is_in_paste_burst());
|
||||
assert_eq!(composer.textarea.text(), "paste");
|
||||
assert_eq!(composer.draft.textarea.text(), "paste");
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert!(!composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "paste");
|
||||
assert_eq!(composer.draft.textarea.text(), "paste");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -918,14 +930,14 @@ mod tests {
|
||||
for ch in ['m', 'a', 't', 'c', 'h'] {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
}
|
||||
assert_eq!(composer.textarea.text(), "oldest matching entry");
|
||||
assert_eq!(composer.draft.textarea.text(), "oldest matching entry");
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(!composer.history_search_active());
|
||||
assert!(composer.textarea.is_empty());
|
||||
assert!(composer.draft.textarea.is_empty());
|
||||
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
assert_eq!(composer.textarea.text(), "newest entry");
|
||||
assert_eq!(composer.draft.textarea.text(), "newest entry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -950,7 +962,7 @@ mod tests {
|
||||
}
|
||||
|
||||
assert!(composer.history_search_active());
|
||||
assert_eq!(composer.textarea.text(), "draft");
|
||||
assert_eq!(composer.draft.textarea.text(), "draft");
|
||||
assert_eq!(composer.footer_mode(), FooterMode::HistorySearch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,18 +808,20 @@ impl BottomPane {
|
||||
self.composer.current_text()
|
||||
}
|
||||
|
||||
pub(crate) fn composer_draft_snapshot(&self) -> chat_composer::ComposerDraftSnapshot {
|
||||
self.composer.draft_snapshot()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn composer_text_elements(&self) -> Vec<TextElement> {
|
||||
self.composer.text_elements()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn composer_local_images(&self) -> Vec<LocalImageAttachment> {
|
||||
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()
|
||||
@@ -865,6 +867,7 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn remote_image_urls(&self) -> Vec<String> {
|
||||
self.composer.remote_image_urls()
|
||||
}
|
||||
|
||||
@@ -152,12 +152,13 @@ impl ChatWidget {
|
||||
return None;
|
||||
}
|
||||
|
||||
let composer = self.bottom_pane.composer_draft_snapshot();
|
||||
let existing_message = UserMessage {
|
||||
text: self.bottom_pane.composer_text(),
|
||||
text_elements: self.bottom_pane.composer_text_elements(),
|
||||
local_images: self.bottom_pane.composer_local_images(),
|
||||
remote_image_urls: self.bottom_pane.remote_image_urls(),
|
||||
mention_bindings: self.bottom_pane.composer_mention_bindings(),
|
||||
text: composer.text,
|
||||
text_elements: composer.text_elements,
|
||||
local_images: composer.local_images,
|
||||
remote_image_urls: composer.remote_image_urls,
|
||||
mention_bindings: composer.mention_bindings,
|
||||
};
|
||||
|
||||
let rejected_messages = self
|
||||
@@ -236,13 +237,14 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn capture_thread_input_state(&self) -> Option<ThreadInputState> {
|
||||
let draft = self.bottom_pane.composer_draft_snapshot();
|
||||
let composer = ThreadComposerState {
|
||||
text: self.bottom_pane.composer_text(),
|
||||
text_elements: self.bottom_pane.composer_text_elements(),
|
||||
local_images: self.bottom_pane.composer_local_images(),
|
||||
remote_image_urls: self.bottom_pane.remote_image_urls(),
|
||||
mention_bindings: self.bottom_pane.composer_mention_bindings(),
|
||||
pending_pastes: self.bottom_pane.composer_pending_pastes(),
|
||||
text: draft.text,
|
||||
text_elements: draft.text_elements,
|
||||
local_images: draft.local_images,
|
||||
remote_image_urls: draft.remote_image_urls,
|
||||
mention_bindings: draft.mention_bindings,
|
||||
pending_pastes: draft.pending_pastes,
|
||||
};
|
||||
Some(ThreadInputState {
|
||||
composer: composer.has_content().then_some(composer),
|
||||
|
||||
Reference in New Issue
Block a user