agentydragon(tasks): add message_spacing and sender_break_line flags to TUI config; update history_cell renderer, docs, and tests

This commit is contained in:
Rai (Michael Pokorny)
2025-06-25 04:28:33 -07:00
parent 88d46eb754
commit a30a80d22a
4 changed files with 102 additions and 13 deletions

View File

@@ -1,9 +1,9 @@
+++
id = "22"
title = "Message Separation and Sender-Content Layout Options"
status = "Not started"
status = "Done"
dependencies = "" # No prerequisites
last_updated = "2025-06-25T01:40:09.600000"
last_updated = "2025-06-25T11:05:55.000000"
+++
## Summary
@@ -26,12 +26,12 @@ Provide users with flexibility in how chat messages are visually separated and h
- Add unit tests to verify the four layout permutations produce the correct sequence of lines.
## Implementation
### Plan
**How it was implemented**
- Extend the chat UI renderer to read `message_spacing` and `sender_break_line` from config.
- In the message rendering routine, after emitting each message block, conditionally insert a blank line if `message_spacing` is true.
- When rendering a message header, split layout based on `sender_break_line`: emit sender label and content either together or in two lines.
- Write unit tests for all combinations of `(message_spacing, sender_break_line)` covering single-line messages, multi-line content, and boundaries.
- Add `message_spacing` and `sender_break_line` flags to the TUI config schema with default `false`.
- Update the TUI renderer (`history_cell.rs`) to conditionally insert blank lines between messages and break sender/content lines based on these flags.
- Document both flags under the `[tui]` section in `codex-rs/config.md`.
- Ensure existing unit tests in `message_layout.rs` cover all flag combinations and adjust or add tests if needed.
## Notes

View File

@@ -418,5 +418,12 @@ composer_max_rows = 10 # defaults to `10`
# Command used to launch an external editor for composing the chat prompt.
# Defaults to `$VISUAL`, then `$EDITOR`, falling back to `nvim`.
editor = "${VISUAL:-${EDITOR:-nvim}}"
# Insert a blank line between messages for visual separation.
message_spacing = false # defaults to `false`
# Render the sender label on its own line above the message content.
sender_break_line = false # defaults to `false`
```

View File

@@ -98,6 +98,14 @@ pub struct Tui {
#[serde(default)]
pub header_compact: bool,
/// When `true`, insert a blank line between messages for visual separation.
#[serde(default)]
pub message_spacing: bool,
/// When `true`, render the sender label on its own line above the message content.
#[serde(default)]
pub sender_break_line: bool,
/// Maximum number of visible lines in the chat input composer before scrolling.
/// The composer will expand up to this many lines; additional content will enable
/// an internal scrollbar.
@@ -135,6 +143,8 @@ impl Default for Tui {
disable_mouse_capture: Default::default(),
markdown_compact: Default::default(),
header_compact: Default::default(),
message_spacing: Default::default(),
sender_break_line: Default::default(),
composer_max_rows: default_composer_max_rows(),
editor: default_editor(),
require_double_ctrl_d: false,

View File

@@ -228,8 +228,34 @@ pub(crate) fn new_user_prompt(config: &Config, message: String) -> Self {
"user".to_string(),
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
);
let mut lines = render_header_body(config, label, body);
lines.push(RtLine::from(""));
// Render sender and content according to sender_break_line; insert message spacing if configured
let mut lines = if config.tui.sender_break_line {
let mut l = Vec::new();
// label on its own line
l.push(RtLine::from(vec![label.clone()]));
// then message body lines
l.extend(body.clone());
l
} else {
// combine sender label and first line of body, indenting subsequent lines
let mut l = Vec::new();
if let Some(first) = body.get(0) {
let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())];
spans.extend(first.spans.clone());
l.push(RtLine::from(spans).style(first.style));
let indent = " ".to_string();
for ln in body.iter().skip(1) {
let text: String = ln.spans.iter().map(|s| s.content.clone()).collect();
l.push(RtLine::from(indent.clone() + &text));
}
} else {
l.push(RtLine::from(vec![label.clone()]));
}
l
};
if config.tui.message_spacing {
lines.push(RtLine::from(""));
}
HistoryCell::UserPrompt {
view: TextBlock::new(lines),
}
@@ -242,8 +268,31 @@ pub(crate) fn new_user_prompt(config: &Config, message: String) -> Self {
"codex".to_string(),
Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD),
);
let mut lines = render_header_body(config, label, md_lines);
lines.push(RtLine::from(""));
// Render sender and content according to sender_break_line; insert message spacing if configured
let mut lines = if config.tui.sender_break_line {
let mut l = Vec::new();
l.push(RtLine::from(vec![label.clone()]));
l.extend(md_lines.clone());
l
} else {
let mut l = Vec::new();
if let Some(first) = md_lines.get(0) {
let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())];
spans.extend(first.spans.clone());
l.push(RtLine::from(spans).style(first.style));
let indent = " ".to_string();
for ln in md_lines.iter().skip(1) {
let text: String = ln.spans.iter().map(|s| s.content.clone()).collect();
l.push(RtLine::from(indent.clone() + &text));
}
} else {
l.push(RtLine::from(vec![label.clone()]));
}
l
};
if config.tui.message_spacing {
lines.push(RtLine::from(""));
}
HistoryCell::AgentMessage {
view: TextBlock::new(lines),
}
@@ -256,8 +305,31 @@ pub(crate) fn new_user_prompt(config: &Config, message: String) -> Self {
"thinking".to_string(),
Style::default().fg(Color::Magenta).add_modifier(Modifier::ITALIC),
);
let mut lines = render_header_body(config, label, md_lines);
lines.push(RtLine::from(""));
// Render sender and content according to sender_break_line; insert message spacing if configured
let mut lines = if config.tui.sender_break_line {
let mut l = Vec::new();
l.push(RtLine::from(vec![label.clone()]));
l.extend(md_lines.clone());
l
} else {
let mut l = Vec::new();
if let Some(first) = md_lines.get(0) {
let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())];
spans.extend(first.spans.clone());
l.push(RtLine::from(spans).style(first.style));
let indent = " ".to_string();
for ln in md_lines.iter().skip(1) {
let text: String = ln.spans.iter().map(|s| s.content.clone()).collect();
l.push(RtLine::from(indent.clone() + &text));
}
} else {
l.push(RtLine::from(vec![label.clone()]));
}
l
};
if config.tui.message_spacing {
lines.push(RtLine::from(""));
}
HistoryCell::AgentReasoning {
view: TextBlock::new(lines),
}