Wrap built-in TUI status lines within the footer row budget (#60682)

## Why

Long built-in status lines were truncated to a single row even when the footer had room for more content.

## What changed

- Wrap status text to the available width and row budget, preserving styles and hyperlink targets across rows.
- Show an unlinked ellipsis when content exceeds the available space.
- Refresh wrapped content when the status text, agent label, or width changes.
- Keep reasoning-effort transitions on one row and restore wrapping after the final animation frame.

## Testing

Add coverage for wrapping and hyperlink boundaries across footer layouts, content and agent-label updates, resizing, and wrapping restoration after an effort transition. Update transcript and startup snapshots for multiline status text.

GitOrigin-RevId: 30e0795c473efd62fc4d0dca4deb6029b625a9e8
This commit is contained in:
Joe Savona
2026-11-10 23:35:28 +00:00
committed by copyberry
parent 192e2ef3ce
commit 65dbdb5f03
17 changed files with 380 additions and 45 deletions
@@ -503,6 +503,8 @@ async fn owned_drag_stops_when_focus_or_input_ownership_is_lost() -> Result<()>
async fn owned_details_keep_the_composer_cursor_and_screen() -> Result<()> {
let mut app = crate::app::test_support::make_test_app().await;
attach_thread(&mut app, ThreadId::new());
app.chat_widget
.set_status_line(Some("gpt-test default · /tmp/project".into()));
app.transcript_cells = vec![user_cell("First prompt"), user_cell("Second prompt")];
app.chat_widget
.apply_external_edit("preserved draft".to_string());
@@ -647,8 +649,7 @@ async fn owned_details_keep_the_composer_cursor_and_screen() -> Result<()> {
app.transcript_view.link_at(column, row).as_deref(),
Some("https://example.com/docs")
);
let copied_cursor = tui.terminal.last_known_cursor_pos;
assert_eq!((copied_cursor.x, copied_cursor.y + 1), (caret.x, caret.y));
assert_eq!(tui.terminal.last_known_cursor_pos, caret);
let buffer = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal);
for pos in Rect::from(outer)
.positions()
@@ -33,7 +33,6 @@ transcript row 39
tab to queue mes
6 columns, 18 rows
ript
row 38
transc
ript
@@ -49,7 +48,8 @@ row 39
her
e
GPT…
GPT-
5.6…
tab
80 columns, 6 rows
@@ -4,7 +4,6 @@ expression: normalize_snapshot_paths(screen)
---
····································
····································
····· ·····
·····› Second prompt ·····
····· ·····
····· ·····
@@ -17,6 +16,7 @@ expression: normalize_snapshot_paths(screen)
····· ·····
·····› preserved draft ·····
····· ·····
····· gpt-test default · /tmp…·····
····· gpt-test default · /tmp/·····
····· project ·····
····································
····································
@@ -40,8 +40,8 @@ expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots.
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/pr…
GPT-5.6-Sol default · /tmp/
project
? for shortcuts
@@ -41,6 +41,8 @@ async fn owned_startup_hides_tip_in_transcript() -> Result<()> {
];
let mut tui = crate::tui::test_support::make_test_tui()?;
tui.set_owned_screen(/*owned*/ true)?;
app.chat_widget
.set_status_line(Some("GPT-5.6-Sol default · /tmp/project".into()));
let mut snapshots = Vec::new();
for width in [80, 32] {
let size = Size::new(width, /*height*/ 20);
+2
View File
@@ -248,6 +248,8 @@ async fn turn_tip_placements_and_completion_barrier() -> Result<()> {
}
}
assert_eq!(saw_barrier, !working);
app.chat_widget
.set_status_line(Some("gpt-test default · /tmp/project".into()));
let frames = tui.frame_requester();
let mut painted = Default::default();
tui.draw(height, |frame| {