Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
11f3f7e
lighter rendering of user/agent messages
nornagon-openai Aug 26, 2025
9c659d6
wip
nornagon-openai Aug 27, 2025
8170786
wip
nornagon-openai Aug 27, 2025
1e56853
wip
nornagon-openai Aug 27, 2025
54dee3a
simplify
nornagon-openai Aug 27, 2025
792fb82
fix tests
nornagon-openai Aug 27, 2025
5159b46
agent message >
nornagon-openai Aug 27, 2025
f51eda8
warning emoji
nornagon-openai Aug 27, 2025
b2d79b9
single-line command runs
nornagon-openai Aug 27, 2025
bb45161
command wrapping
nornagon-openai Aug 27, 2025
93d9da0
diff wip
nornagon-openai Aug 28, 2025
e6ba0af
diffs
nornagon-openai Aug 28, 2025
ca93201
fix wrapping
nornagon-openai Aug 28, 2025
a25a6be
cleanup
nornagon-openai Aug 28, 2025
2567354
wip
nornagon-openai Aug 29, 2025
99dd4f2
fix spacing in textarea
nornagon-openai Aug 29, 2025
8a86495
tui: show spinner instead of bullet in Exploring header
nornagon-openai Aug 29, 2025
7ecac28
test
nornagon-openai Aug 29, 2025
e7548e2
revert status changes
nornagon-openai Aug 29, 2025
1bc20d1
placeholder
nornagon-openai Aug 29, 2025
990e997
update plan, approval spacing
nornagon-openai Aug 29, 2025
3339c3b
Merge remote-tracking branch 'origin/main' into nornagon/message-styling
nornagon-openai Aug 29, 2025
599e5d0
fix testst
nornagon-openai Aug 29, 2025
64d548b
tui: ChatComposer rect fixes and hint line; split patch history snaps…
nornagon-openai Aug 29, 2025
49d8633
remove empty test
nornagon-openai Aug 29, 2025
3a10412
test
nornagon-openai Aug 29, 2025
0f18de3
simpler test
nornagon-openai Aug 29, 2025
230bd1a
more simpler
nornagon-openai Aug 29, 2025
b8adb5a
test
nornagon-openai Aug 29, 2025
b6d38e9
tui: parse bash commands and dim some syntax
nornagon-openai Aug 29, 2025
cdfd879
fix headers
nornagon-openai Aug 29, 2025
7afe05a
better wrapping
nornagon-openai Aug 29, 2025
c312e26
diff simplify
nornagon-openai Aug 29, 2025
5bc0d64
simplify
nornagon-openai Aug 29, 2025
3da7f52
better path display
nornagon-openai Aug 29, 2025
a8d37e0
simplify diff
nornagon-openai Aug 29, 2025
d7ed1fd
better wrap in userhistorycell
nornagon-openai Aug 29, 2025
582d7fd
simplify
nornagon-openai Aug 29, 2025
27ef3c5
revert changes in tui.rs
nornagon-openai Aug 29, 2025
c21e10a
show single file for change approved
nornagon-openai Aug 29, 2025
0d96cb8
reparse commands for ideal response
nornagon-openai Aug 29, 2025
521a71b
dim output text
nornagon-openai Aug 29, 2025
861f7b5
convert binary size test to snapshot
nornagon-openai Aug 29, 2025
70b83f4
dim output text
nornagon-openai Aug 29, 2025
1aa8cec
don't try to detect image paths for normal typing
nornagon-openai Aug 30, 2025
93934a1
defensively, assume 1-char strings are not image paths
nornagon-openai Aug 30, 2025
1dbfa1f
Merge branch 'nornagon/lag' into nornagon/message-styling
nornagon-openai Aug 30, 2025
bf72447
update plan cell
nornagon-openai Sep 1, 2025
e8fc8c2
simplify
nornagon-openai Sep 1, 2025
1677132
wip
nornagon-openai Sep 1, 2025
47c9068
Merge branch 'nornagon/message-styling' into nornagon/parse-bash
nornagon-openai Sep 2, 2025
34bdfdd
Merge remote-tracking branch 'origin/main' into nornagon/parse-bash
nornagon-openai Sep 2, 2025
e700c44
Merge remote-tracking branch 'origin/main' into nornagon/parse-bash
nornagon-openai Sep 2, 2025
62c8ee3
fix
nornagon-openai Sep 2, 2025
19fd5e6
standardize on allowing hyphenation and breaking words
nornagon-openai Sep 2, 2025
59a4c63
restore no-hyphenation for bash
nornagon-openai Sep 2, 2025
92144d9
wip
nornagon-openai Sep 2, 2025
3750b6a
wip
nornagon-openai Sep 3, 2025
e2688ff
wip
nornagon-openai Sep 3, 2025
fbefb73
Merge remote-tracking branch 'origin/main' into nornagon/parse-bash
nornagon-openai Sep 3, 2025
f85f54e
wip
nornagon-openai Sep 3, 2025
6ca03b6
reorg
nornagon-openai Sep 4, 2025
d508cff
wip
nornagon-openai Sep 4, 2025
78d911f
move prefix_lines helper
nornagon-openai Sep 4, 2025
01e311b
gitignore
nornagon-openai Sep 4, 2025
54630ca
order
nornagon-openai Sep 4, 2025
79c66d9
consolidate wrapping helpers
nornagon-openai Sep 4, 2025
7508ec2
agents.md
nornagon-openai Sep 4, 2025
d9fd052
lint
nornagon-openai Sep 4, 2025
9f52dd2
better test
nornagon-openai Sep 4, 2025
c11f63a
simplify
nornagon-openai Sep 4, 2025
9e28bb4
tests
nornagon-openai Sep 4, 2025
8d2eeb1
Merge branch 'main' into nornagon/parse-bash
nornagon-openai Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions codex-rs/apply-patch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ pub enum ApplyPatchFileChange {
Add {
content: String,
},
Delete,
Delete {
content: String,
},
Update {
unified_diff: String,
move_path: Option<PathBuf>,
Expand Down Expand Up @@ -210,7 +212,18 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
changes.insert(path, ApplyPatchFileChange::Add { content: contents });
}
Hunk::DeleteFile { .. } => {
changes.insert(path, ApplyPatchFileChange::Delete);
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(e) => {
return MaybeApplyPatchVerified::CorrectnessError(
ApplyPatchError::IoError(IoError {
context: format!("Failed to read {}", path.display()),
source: e,
}),
);
}
};
changes.insert(path, ApplyPatchFileChange::Delete { content });
}
Hunk::UpdateFile {
move_path, chunks, ..
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/src/apply_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ pub(crate) fn convert_apply_patch_to_protocol(
ApplyPatchFileChange::Add { content } => FileChange::Add {
content: content.clone(),
},
ApplyPatchFileChange::Delete => FileChange::Delete,
ApplyPatchFileChange::Delete { content } => FileChange::Delete {
content: content.clone(),
},
ApplyPatchFileChange::Update {
unified_diff,
move_path,
Expand Down
36 changes: 31 additions & 5 deletions codex-rs/core/src/git_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,35 @@ use tokio::process::Command;
use tokio::time::Duration as TokioDuration;
use tokio::time::timeout;

use crate::util::is_inside_git_repo;
/// Return `true` if the project folder specified by the `Config` is inside a
/// Git repository.
///
/// The check walks up the directory hierarchy looking for a `.git` file or
/// directory (note `.git` can be a file that contains a `gitdir` entry). This
/// approach does **not** require the `git` binary or the `git2` crate and is
/// therefore fairly lightweight.
///
/// Note that this does **not** detect *work‑trees* created with
/// `git worktree add` where the checkout lives outside the main repository
/// directory. If you need Codex to work from such a checkout simply pass the
/// `--allow-no-git-exec` CLI flag that disables the repo requirement.
pub fn get_git_repo_root(base_dir: &Path) -> Option<PathBuf> {
let mut dir = base_dir.to_path_buf();

loop {
if dir.join(".git").exists() {
return Some(dir);
}

// Pop one component (go up one directory). `pop` returns false when
// we have reached the filesystem root.
if !dir.pop() {
break;
}
}

None
}

/// Timeout for git commands to prevent freezing on large repositories
const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5);
Expand Down Expand Up @@ -94,9 +122,7 @@ pub async fn collect_git_info(cwd: &Path) -> Option<GitInfo> {

/// Returns the closest git sha to HEAD that is on a remote as well as the diff to that sha.
pub async fn git_diff_to_remote(cwd: &Path) -> Option<GitDiffToRemote> {
if !is_inside_git_repo(cwd) {
return None;
}
get_git_repo_root(cwd)?;

let remotes = get_git_remotes(cwd).await?;
let branches = branch_ancestry(cwd).await?;
Expand Down Expand Up @@ -440,7 +466,7 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
}

/// Resolve the path that should be used for trust checks. Similar to
/// `[utils::is_inside_git_repo]`, but resolves to the root of the main
/// `[get_git_repo_root]`, but resolves to the root of the main
/// repository. Handles worktrees.
pub fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> {
let base = if cwd.is_dir() { cwd } else { cwd.parent()? };
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ fn is_write_patch_constrained_to_writable_paths(

for (path, change) in action.changes() {
match change {
ApplyPatchFileChange::Add { .. } | ApplyPatchFileChange::Delete => {
ApplyPatchFileChange::Add { .. } | ApplyPatchFileChange::Delete { .. } => {
if !is_path_writable(path) {
return false;
}
Expand Down
14 changes: 12 additions & 2 deletions codex-rs/core/src/turn_diff_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,12 @@ index {ZERO_OID}..{right_oid}
fs::write(&file, "x\n").unwrap();

let mut acc = TurnDiffTracker::new();
let del_changes = HashMap::from([(file.clone(), FileChange::Delete)]);
let del_changes = HashMap::from([(
file.clone(),
FileChange::Delete {
content: "x\n".to_string(),
},
)]);
acc.on_patch_begin(&del_changes);

// Simulate apply: delete the file from disk.
Expand Down Expand Up @@ -741,7 +746,12 @@ index {left_oid}..{right_oid}
assert_eq!(first, expected_first);

// Next: introduce a brand-new path b.txt into baseline snapshots via a delete change.
let del_b = HashMap::from([(b.clone(), FileChange::Delete)]);
let del_b = HashMap::from([(
b.clone(),
FileChange::Delete {
content: "z\n".to_string(),
},
)]);
acc.on_patch_begin(&del_b);
// Simulate apply: delete b.txt.
let baseline_mode = file_mode_for_path(&b).unwrap_or(FileMode::Regular);
Expand Down
31 changes: 0 additions & 31 deletions codex-rs/core/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::path::Path;
use std::time::Duration;

use rand::Rng;
Expand All @@ -12,33 +11,3 @@ pub(crate) fn backoff(attempt: u64) -> Duration {
let jitter = rand::rng().random_range(0.9..1.1);
Duration::from_millis((base as f64 * jitter) as u64)
}

/// Return `true` if the project folder specified by the `Config` is inside a
/// Git repository.
///
/// The check walks up the directory hierarchy looking for a `.git` file or
/// directory (note `.git` can be a file that contains a `gitdir` entry). This
/// approach does **not** require the `git` binary or the `git2` crate and is
/// therefore fairly lightweight.
///
/// Note that this does **not** detect *work‑trees* created with
/// `git worktree add` where the checkout lives outside the main repository
/// directory. If you need Codex to work from such a checkout simply pass the
/// `--allow-no-git-exec` CLI flag that disables the repo requirement.
pub fn is_inside_git_repo(base_dir: &Path) -> bool {
let mut dir = base_dir.to_path_buf();

loop {
if dir.join(".git").exists() {
return true;
}

// Pop one component (go up one directory). `pop` returns false when
// we have reached the filesystem root.
if !dir.pop() {
break;
}
}

false
}
7 changes: 5 additions & 2 deletions codex-rs/exec/src/event_processor_with_human_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,16 @@ impl EventProcessor for EventProcessorWithHumanOutput {
println!("{}", line.style(self.green));
}
}
FileChange::Delete => {
FileChange::Delete { content } => {
let header = format!(
"{} {}",
format_file_change(change),
path.to_string_lossy()
);
println!("{}", header.style(self.magenta));
for line in content.lines() {
println!("{}", line.style(self.red));
}
}
FileChange::Update {
unified_diff,
Expand Down Expand Up @@ -560,7 +563,7 @@ fn escape_command(command: &[String]) -> String {
fn format_file_change(change: &FileChange) -> &'static str {
match change {
FileChange::Add { .. } => "A",
FileChange::Delete => "D",
FileChange::Delete { .. } => "D",
FileChange::Update {
move_path: Some(_), ..
} => "R",
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ use codex_core::ConversationManager;
use codex_core::NewConversation;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::git_info::get_git_repo_root;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::util::is_inside_git_repo;
use codex_login::AuthManager;
use codex_ollama::DEFAULT_OSS_MODEL;
use codex_protocol::config_types::SandboxMode;
Expand Down Expand Up @@ -183,7 +183,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
// is using.
event_processor.print_config_summary(&config, &prompt);

if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
if !skip_git_repo_check && get_git_repo_root(&config.cwd.to_path_buf()).is_none() {
eprintln!("Not inside a trusted directory and --skip-git-repo-check was not specified.");
std::process::exit(1);
}
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,9 @@ pub enum FileChange {
Add {
content: String,
},
Delete,
Delete {
content: String,
},
Update {
unified_diff: String,
move_path: Option<PathBuf>,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ unicode-segmentation = "1.12.0"
unicode-width = "0.1"
url = "2"
uuid = "1"
pathdiff = "0.2"

[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/backtrack_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub(crate) fn highlight_range_for_nth_last_user(
/// Compute the wrapped display-line offset before `header_idx`, for a given width.
pub(crate) fn wrapped_offset_before(lines: &[Line<'_>], header_idx: usize, width: u16) -> usize {
let before = &lines[0..header_idx];
crate::insert_history::word_wrap_lines(before, width).len()
crate::insert_history::word_wrap_lines(before, width as usize).len()
}

/// Find the header index for the Nth last user message in the transcript.
Expand Down
33 changes: 21 additions & 12 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use super::command_popup::CommandPopup;
use super::file_search_popup::FileSearchPopup;
use super::paste_burst::CharDecision;
use super::paste_burst::PasteBurst;
use crate::bottom_pane::paste_burst::FlushResult;
use crate::slash_command::SlashCommand;
use codex_protocol::custom_prompts::CustomPrompt;

Expand Down Expand Up @@ -223,7 +224,7 @@ impl ChatComposer {
let placeholder = format!("[Pasted Content {char_count} chars]");
self.textarea.insert_element(&placeholder);
self.pending_pastes.push((placeholder, pasted));
} else if self.handle_paste_image_path(pasted.clone()) {
} else if char_count > 1 && self.handle_paste_image_path(pasted.clone()) {
self.textarea.insert_str(" ");
} else {
self.textarea.insert_str(&pasted);
Expand Down Expand Up @@ -298,12 +299,7 @@ impl ChatComposer {
}

pub(crate) fn flush_paste_burst_if_due(&mut self) -> bool {
let now = Instant::now();
if let Some(pasted) = self.paste_burst.flush_if_due(now) {
let _ = self.handle_paste(pasted);
return true;
}
false
self.handle_paste_burst_flush(Instant::now())
}

pub(crate) fn is_in_paste_burst(&self) -> bool {
Expand Down Expand Up @@ -848,15 +844,28 @@ impl ChatComposer {
}
}

fn handle_paste_burst_flush(&mut self, now: Instant) -> bool {
match self.paste_burst.flush_if_due(now) {
FlushResult::Paste(pasted) => {
self.handle_paste(pasted);
true
}
FlushResult::Typed(ch) => {
self.textarea.insert_str(ch.to_string().as_str());
true
}
FlushResult::None => false,
}
}

/// Handle generic Input events that modify the textarea content.
fn handle_input_basic(&mut self, input: KeyEvent) -> (InputResult, bool) {
// If we have a buffered non-bracketed paste burst and enough time has
// elapsed since the last char, flush it before handling a new input.
let now = Instant::now();
if let Some(pasted) = self.paste_burst.flush_if_due(now) {
// Reuse normal paste path (handles large-paste placeholders).
self.handle_paste(pasted);
}
let flush_start = Instant::now();
self.handle_paste_burst_flush(now);
let flush_ms = flush_start.elapsed().as_secs_f64() * 1000.0;

// If we're capturing a burst and receive Enter, accumulate it instead of inserting.
if matches!(input.code, KeyCode::Enter)
Expand Down Expand Up @@ -1313,7 +1322,7 @@ impl WidgetRef for ChatComposer {
);
let mut textarea_rect = textarea_rect;
textarea_rect.width = textarea_rect.width.saturating_sub(1);
textarea_rect.x = textarea_rect.x.saturating_add(1);
textarea_rect.x += 1;

let mut state = self.textarea_state.borrow_mut();
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
Expand Down
6 changes: 3 additions & 3 deletions codex-rs/tui/src/bottom_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ mod tests {

pane.set_task_running(true);

// Height=2 → composer visible; status is hidden to preserve composer. Spacer may collapse.
// Height=2 → status on one row, composer on the other.
let area2 = Rect::new(0, 0, 20, 2);
let mut buf2 = Buffer::empty(area2);
(&pane).render_ref(area2, &mut buf2);
Expand All @@ -715,8 +715,8 @@ mod tests {
"expected composer to be visible on one of the rows: row0={row0:?}, row1={row1:?}"
);
assert!(
!row0.contains("Working") && !row1.contains("Working"),
"status header should be hidden when height=2"
row0.contains("Working") || row1.contains("Working"),
"expected status header to be visible at height=2: row0={row0:?}, row1={row1:?}"
);

// Height=1 → no padding; single row is the composer (status hidden).
Expand Down
Loading