Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix headers
  • Loading branch information
nornagon-openai committed Aug 29, 2025
commit cdfd879b245852480a3fe7f070c57d0157743b16
2 changes: 1 addition & 1 deletion codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,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
62 changes: 23 additions & 39 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ pub(crate) struct ChatWidget {
// Stream lifecycle controller
stream: StreamController,
running_commands: HashMap<String, RunningCommand>,
// No longer needed; completions are stored within the active ExecCell
task_complete_pending: bool,
// Queue of interruptive UI events deferred during an active write cycle
interrupts: InterruptManager,
Expand Down Expand Up @@ -441,15 +440,13 @@ impl ChatWidget {
self.task_complete_pending = false;
}
// A completed stream indicates non-exec content was just inserted.
// Exec grouping relies on active_exec_cell now; nothing to toggle.
self.flush_interrupt_queue();
}
}

#[inline]
fn handle_streaming_delta(&mut self, delta: String) {
// Before streaming agent content, flush any active exec cell group.
tracing::info!(target: "codex_tui::exec", "flushing active_exec_cell: reason=agent_stream_delta");
self.flush_active_exec_cell();
let sink = AppEventHistorySink(self.app_event_tx.clone());
self.stream.begin(&sink);
Expand All @@ -464,8 +461,9 @@ impl ChatWidget {
None => (vec![ev.call_id.clone()], Vec::new()),
};

// Ensure we have an active cell, then record completion into it.
if self.active_exec_cell.is_none() {
// This should have been created by handle_exec_begin_now, but in case it wasn't,
// create it now.
self.active_exec_cell = Some(history_cell::new_active_exec_command(
ev.call_id.clone(),
command,
Expand Down Expand Up @@ -493,9 +491,9 @@ impl ChatWidget {
&mut self,
event: codex_core::protocol::PatchApplyEndEvent,
) {
if event.success {
// Suppress success block per new styling spec (no "✓ Applied patch" block)
} else {
// If the patch was successful, just let the "Edited" block stand.
// Otherwise, add a failure block.
if !event.success {
self.add_to_history(history_cell::new_patch_apply_failure(event.stderr));
}
}
Expand Down Expand Up @@ -541,32 +539,28 @@ impl ChatWidget {
parsed_cmd: ev.parsed_cmd.clone(),
},
);
// Accumulate each exec begin as a new call within the active Exec cell.
match self.active_exec_cell.as_mut() {
Some(exec) => {
if let Some(new_exec) = exec.with_added_call(
ev.call_id.clone(),
ev.command.clone(),
ev.parsed_cmd.clone(),
) {
self.active_exec_cell = Some(new_exec);
} else {
// Make a new cell.
self.flush_active_exec_cell();
self.active_exec_cell = Some(history_cell::new_active_exec_command(
ev.call_id.clone(),
ev.command.clone(),
ev.parsed_cmd.clone(),
));
}
}
_ => {
if let Some(exec) = &self.active_exec_cell {
if let Some(new_exec) = exec.with_added_call(
ev.call_id.clone(),
ev.command.clone(),
ev.parsed_cmd.clone(),
) {
self.active_exec_cell = Some(new_exec);
} else {
// Make a new cell.
self.flush_active_exec_cell();
self.active_exec_cell = Some(history_cell::new_active_exec_command(
ev.call_id.clone(),
ev.command.clone(),
ev.parsed_cmd.clone(),
));
}
} else {
self.active_exec_cell = Some(history_cell::new_active_exec_command(
ev.call_id.clone(),
ev.command.clone(),
ev.parsed_cmd.clone(),
));
}

// Request a redraw so the working header and command list are visible immediately.
Expand Down Expand Up @@ -638,7 +632,6 @@ impl ChatWidget {
last_token_usage: TokenUsage::default(),
stream: StreamController::new(config),
running_commands: HashMap::new(),

task_complete_pending: false,
interrupts: InterruptManager::new(),
reasoning_buffer: String::new(),
Expand Down Expand Up @@ -683,7 +676,6 @@ impl ChatWidget {
last_token_usage: TokenUsage::default(),
stream: StreamController::new(config),
running_commands: HashMap::new(),

task_complete_pending: false,
interrupts: InterruptManager::new(),
reasoning_buffer: String::new(),
Expand Down Expand Up @@ -900,24 +892,16 @@ impl ChatWidget {

fn flush_active_exec_cell(&mut self) {
if let Some(active) = self.active_exec_cell.take() {
tracing::info!(
target = "codex_tui::exec",
"flush_active_exec_cell: calls={}",
active.calls_len(),
);
self.app_event_tx
.send(AppEvent::InsertHistoryCell(Box::new(active)));
}
}

fn add_to_history(&mut self, cell: impl HistoryCell + 'static) {
// Only break exec grouping if the cell renders visible lines.
let has_display_lines = !cell.display_lines(u16::MAX).is_empty();
if has_display_lines {
tracing::info!(target: "codex_tui::exec", "flushing active_exec_cell: reason=insert_other_history_cell");
if !cell.display_lines(u16::MAX).is_empty() {
// Only break exec grouping if the cell renders visible lines.
self.flush_active_exec_cell();
}
// Exec grouping relies on active_exec_cell now; nothing to toggle.
self.app_event_tx
.send(AppEvent::InsertHistoryCell(Box::new(cell)));
}
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,8 +952,8 @@ fn apply_patch_events_emit_history_cells() {
assert!(!cells.is_empty(), "expected apply block cell to be sent");
let blob = lines_to_single_string(cells.last().unwrap());
assert!(
blob.contains("Edited foo.txt"),
"expected single-file edited header with filename: {blob:?}"
blob.contains("Added foo.txt") || blob.contains("Edited foo.txt"),
"expected single-file header with filename (Added/Edited): {blob:?}"
);

// 3) End apply success -> success cell
Expand Down
15 changes: 13 additions & 2 deletions codex-rs/tui/src/diff_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,18 @@ fn render_changes_block(
}
}
HeaderKind::Edited => {
header_spans.push("Edited".bold());
// For a single file, specialize the verb based on the change kind.
// Otherwise, use the generic "Edited" summary.
let verb = if single_file_inline {
match first_row_opt.as_ref().map(|r| &r.change) {
Some(FileChange::Add { .. }) => "Added",
Some(FileChange::Delete) => "Deleted",
_ => "Edited",
}
} else {
"Edited"
};
header_spans.push(verb.bold());
if single_file_inline {
if let Some(fr) = &first_row_opt {
header_spans.push(format!(" {} ", fr.display).into());
Expand Down Expand Up @@ -810,7 +821,7 @@ mod tests {
// This mirrors the desired layout example: sign only on first inserted line,
// subsequent wrapped pieces start aligned under the line number gutter.
let original = "1\n2\n3\n4\n";
let modified = "1\nadded line which wraps and_if_there_is_a_long_token_it_will_be_broken\n3\n4 context line which also wraps across\n";
let modified = "1\nadded long line which wraps and_if_there_is_a_long_token_it_will_be_broken\n3\n4 context line which also wraps across\n";
let patch = diffy::create_patch(original, modified).to_string();

let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
Expand Down
50 changes: 48 additions & 2 deletions codex-rs/tui/src/history_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1964,15 +1964,61 @@ mod tests {
.join("\n");
insta::assert_snapshot!(rendered);
}

#[test]
fn ran_cell_multiline_with_stderr_snapshot() {
// Build an exec cell that completes (so it renders as "Ran") with a
// command long enough that it must render on its own line under the
// header, and include a couple of stderr lines to verify the output
// block prefixes and wrapping.
let call_id = "c_wrap_err".to_string();
let long_cmd =
"echo this_is_a_very_long_single_token_that_will_wrap_across_the_available_width";
let mut cell = ExecCell::new(ExecCall {
call_id: call_id.clone(),
command: vec!["bash".into(), "-lc".into(), long_cmd.to_string()],
parsed: Vec::new(),
output: None,
start_time: Some(Instant::now()),
duration: None,
});

let stderr = "error: first line on stderr\nerror: second line on stderr".to_string();
cell.complete_call(
&call_id,
CommandOutput {
exit_code: 1,
stdout: String::new(),
stderr,
formatted_output: String::new(),
},
Duration::from_millis(5),
);

// Narrow width to force the command to render under the header line.
let width: u16 = 28;
let rendered = cell
.display_lines(width)
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn user_history_cell_wraps_and_prefixes_each_line_snapshot() {
let msg = "one two three four five six seven";
let cell = UserHistoryCell {
message: msg.to_string(),
};

// Small width to force wrapping. Effective wrap width is width-1 due to the ▌ prefix.
let width: u16 = 8;
// Small width to force wrapping more clearly. Effective wrap width is width-1 due to the ▌ prefix.
let width: u16 = 12;
let lines = cell.display_lines(width);

let rendered = lines
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
source: tui/src/diff_render.rs
assertion_line: 748
expression: terminal.backend()
---
"• Edited new_file.txt (+2 -0) "
"• Added new_file.txt (+2 -0) "
" 1 +alpha "
" 2 +beta "
" "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
source: tui/src/diff_render.rs
assertion_line: 748
expression: terminal.backend()
---
"• Edited tmp_delete_example.txt (+0 -3) "
"• Deleted tmp_delete_example.txt (+0 -3) "
" 1 -first "
" 2 -second "
" 3 -third "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ expression: text
---
1 1
2 -2
2 +added line which
wraps and_if_ther
e_is_a_long_token
_it_will_be_broke
n
2 +added long line w
hich wraps and_if
_there_is_a_long_
token_it_will_be_
broken
3 3
4 -4
4 +4 context line wh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: tui/src/history_cell.rs
expression: rendered
---
• Ran
└ echo
this_is_a_very_long_sing
le_token_that_will_wrap_
across_the_available_wid
th
error: first line on
stderr
error: second line on
stderr
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ source: tui/src/history_cell.rs
expression: rendered
---
▌one two
▌three
▌four
▌five
▌six
▌three four
▌five six
▌seven
34 changes: 19 additions & 15 deletions codex-rs/tui/src/streaming/controller.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::history_cell;
use crate::history_cell::HistoryCell;
use codex_core::config::Config;
use ratatui::text::Line;

Expand All @@ -6,7 +8,7 @@ use super::StreamState;

/// Sink for history insertions and animation control.
pub(crate) trait HistorySink {
fn insert_history_cell(&self, cell: Box<dyn crate::history_cell::HistoryCell>);
fn insert_history_cell(&self, cell: Box<dyn HistoryCell>);
fn start_commit_animation(&self);
fn stop_commit_animation(&self);
}
Expand Down Expand Up @@ -66,10 +68,6 @@ impl StreamController {
// leave header state unchanged; caller decides when to reset
}

fn emit_header_if_needed(&mut self, out_lines: &mut Lines) -> bool {
self.header.maybe_emit(out_lines)
}

/// Begin an answer stream. Does not emit header yet; it is emitted on first commit.
pub(crate) fn begin(&mut self, _sink: &impl HistorySink) {
// Starting a new stream cancels any pending finish-from-previous-stream animation.
Expand Down Expand Up @@ -124,12 +122,15 @@ impl StreamController {
out_lines.extend(step.history);
}
if !out_lines.is_empty() {
// Determine whether we would have emitted a header for this block.
let mut header_probe: Lines = Vec::new();
let header_emitted = self.emit_header_if_needed(&mut header_probe);
// Insert as a HistoryCell so display drops the header while transcript keeps it.
let cell = crate::history_cell::AgentMessageCell::new(out_lines, header_emitted);
sink.insert_history_cell(Box::new(cell));
let include_header = self.header.should_emit_header();
if include_header {
self.header.mark_emitted();
}
sink.insert_history_cell(Box::new(history_cell::AgentMessageCell::new(
out_lines,
include_header,
)));
}

// Cleanup
Expand Down Expand Up @@ -161,11 +162,14 @@ impl StreamController {
}
let step = { self.state.step() };
if !step.history.is_empty() {
// Decide if a header would be emitted; always wrap in a cell so display can width-wrap and indent.
let mut probe: Lines = Vec::new();
let header_emitted = self.emit_header_if_needed(&mut probe);
let cell = crate::history_cell::AgentMessageCell::new(step.history, header_emitted);
sink.insert_history_cell(Box::new(cell));
let include_header = self.header.should_emit_header();
if include_header {
self.header.mark_emitted();
}
sink.insert_history_cell(Box::new(history_cell::AgentMessageCell::new(
step.history,
include_header,
)));
}

let is_idle = self.state.is_idle();
Expand Down
Loading