Compare commits

..

1 Commits

Author SHA1 Message Date
ae
0850d6ddf8 Check ripgrep availability before suggesting it 2025-07-25 10:46:06 -07:00
5 changed files with 30 additions and 142 deletions

View File

@@ -25,6 +25,7 @@ futures = "0.3"
libc = "0.2.174"
mcp-types = { path = "../mcp-types" }
mime_guess = "2.0"
once_cell = "1"
rand = "0.9"
reqwest = { version = "0.12", features = ["json", "stream"] }
serde = { version = "1", features = ["derive"] }

View File

@@ -12,11 +12,35 @@ use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use tokio::sync::mpsc;
use once_cell::sync::Lazy;
use std::process::Command;
use std::process::Stdio;
/// The `instructions` field in the payload sent to a model should always start
/// with this content.
const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md");
static RG_AVAILABLE: Lazy<bool> = Lazy::new(|| {
Command::new("rg")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
});
const RG_LINE: &str = "- Do not use `ls -R`, `find`, or `grep` - these are slow in large repos. Use `rg` and `rg --files`.";
const RG_LINE_NO_RG: &str = "- Do not use `ls -R`, `find`, or `grep` - these are slow in large repos.";
fn base_instructions() -> Cow<'static, str> {
if *RG_AVAILABLE {
Cow::Borrowed(BASE_INSTRUCTIONS)
} else {
Cow::Owned(BASE_INSTRUCTIONS.replace(RG_LINE, RG_LINE_NO_RG))
}
}
/// API request payload for a single model turn.
#[derive(Default, Debug, Clone)]
pub struct Prompt {
@@ -42,8 +66,9 @@ impl Prompt {
let base = self
.base_instructions_override
.as_deref()
.unwrap_or(BASE_INSTRUCTIONS);
let mut sections: Vec<&str> = vec![base];
.map(Cow::Borrowed)
.unwrap_or_else(|| base_instructions());
let mut sections: Vec<&str> = vec![base.as_ref()];
if let Some(ref user) = self.user_instructions {
sections.push(user);
}

View File

@@ -1,3 +1,4 @@
use crate::tui;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;
@@ -113,9 +114,7 @@ impl LineBuilder {
}
}
use ratatui::backend::Backend;
pub fn insert_history_lines<B: Backend>(terminal: &mut ratatui::Terminal<B>, lines: Vec<Line<'static>>) {
pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
let term_width = terminal.size().map(|a| a.width).unwrap_or(80) as usize;
let mut physical: Vec<Line<'static>> = Vec::new();
@@ -177,5 +176,3 @@ pub fn insert_history_lines<B: Backend>(terminal: &mut ratatui::Terminal<B>, lin
})
.ok();
}
// Tests are implemented as integration tests (see tests/insert_history.rs)

View File

@@ -45,9 +45,6 @@ mod text_formatting;
mod tui;
mod user_approval_widget;
// Re-export for integration tests
pub use insert_history::insert_history_lines;
pub use cli::Cli;
pub fn run_main(

View File

@@ -1,132 +0,0 @@
use codex_tui::insert_history_lines;
use ratatui::backend::TestBackend;
use ratatui::text::{Line, Span};
use ratatui::{Terminal, TerminalOptions, Viewport};
use ratatui::widgets::Paragraph;
// Helper to initialise a terminal with an inline viewport.
fn test_terminal(width: u16, height: u16, bottom_height: u16) -> Terminal<TestBackend> {
Terminal::with_options(
TestBackend::new(width, height),
TerminalOptions { viewport: Viewport::Inline(bottom_height) },
)
.expect("terminal")
}
// Extract the buffer contents as Strings (one per row) trimming trailing spaces.
fn buffer_lines(term: &Terminal<TestBackend>) -> Vec<String> {
let backend = term.backend();
let size = term.size().expect("size");
let mut out = Vec::new();
for y in 0..size.height {
let mut row = String::new();
for x in 0..size.width {
let cell = backend.buffer().get(x, y);
row.push_str(cell.symbol());
}
out.push(row.trim_end().to_string());
}
out
}
#[test]
fn single_line_passthrough() {
let mut term = test_terminal(20, 10, 3); // 7 lines history space
insert_history_lines(&mut term, vec![Line::from("hello world")]);
let lines = buffer_lines(&term);
assert!(lines.iter().any(|l| l.contains("hello world")), "history line visible");
}
#[test]
fn explicit_newlines_preserved() {
let mut term = test_terminal(20, 10, 3);
insert_history_lines(&mut term, vec![Line::from(Span::raw("foo\nbar\n"))]);
let lines = buffer_lines(&term);
assert!(lines.contains(&"foo".to_string()));
assert!(lines.contains(&"bar".to_string()));
assert!(lines.iter().filter(|l| l.is_empty()).count() >= 1);
}
#[test]
fn whitespace_normalisation() {
let mut term = test_terminal(30, 10, 3);
insert_history_lines(
&mut term,
vec![Line::from(vec![Span::raw(" a"), Span::raw("\t\tb"), Span::raw(" c")])],
);
let joined = buffer_lines(&term).join("\n");
assert!(joined.contains("a b c"));
}
#[test]
fn soft_wrapping() {
let mut term = test_terminal(10, 10, 3);
insert_history_lines(&mut term, vec![Line::from("hello world test")]);
let lines = buffer_lines(&term);
assert!(lines.iter().any(|l| l == "hello"));
assert!(lines.iter().any(|l| l == "world test"));
}
#[test]
fn overlong_word_splitting() {
let mut term = test_terminal(5, 10, 3);
insert_history_lines(&mut term, vec![Line::from("abcdefgh")]);
let lines = buffer_lines(&term);
assert!(lines.iter().any(|l| l == "abcde"));
assert!(lines.iter().any(|l| l == "fgh"));
}
#[test]
fn whitespace_collapse_across_spans() {
let mut term = test_terminal(20, 10, 3);
insert_history_lines(&mut term, vec![Line::from(vec![Span::raw("foo "), Span::raw(" bar")])]);
let joined = buffer_lines(&term).join("\n");
assert!(joined.contains("foo bar"));
assert!(!joined.contains("foo bar"));
}
#[test]
fn trailing_newline_preserved() {
let mut term = test_terminal(20, 10, 3);
insert_history_lines(&mut term, vec![Line::from(Span::raw("xyz\n"))]);
let lines = buffer_lines(&term);
assert!(lines.contains(&"xyz".to_string()));
assert!(lines.iter().filter(|l| l.is_empty()).count() >= 1);
}
#[test]
fn wide_unicode_wrapping() {
let mut term = test_terminal(6, 10, 3);
insert_history_lines(&mut term, vec![Line::from("")]);
let lines = buffer_lines(&term);
assert!(lines.iter().any(|l| l.contains(" ")));
assert!(lines.iter().any(|l| l.contains(" ")));
}
#[test]
fn sequential_insertions_order() {
let mut term = test_terminal(20, 10, 3);
insert_history_lines(&mut term, vec![Line::from("first")]);
insert_history_lines(&mut term, vec![Line::from("second")]);
let lines = buffer_lines(&term);
let mut first_idx = None;
let mut second_idx = None;
for (i, l) in lines.iter().enumerate() {
if l.contains("first") { first_idx = Some(i); }
if l.contains("second") { second_idx = Some(i); }
}
let (Some(fi), Some(si)) = (first_idx, second_idx) else { panic!("missing lines") };
assert!(fi < si, "expected 'first' above 'second'");
}
#[test]
fn integration_bottom_viewport_render() {
let mut term = test_terminal(15, 8, 3);
insert_history_lines(&mut term, vec![Line::from("history one"), Line::from("history two")]);
term.draw(|f| f.render_widget(Paragraph::new("bottom"), f.area())).unwrap();
let lines = buffer_lines(&term);
assert!(lines.iter().any(|l| l.contains("history one")));
assert!(lines.iter().any(|l| l.contains("history two")));
assert!(lines.iter().any(|l| l.contains("bottom")));
}