fix(tui): keep the turn clock when the session half is suppressed (#6084)

This commit is contained in:
CodeWhale Bot committed 2026-09-12 05:54:43 +00:00
1 parent f8f3211945
commit 02929ddc09
3 files changed
+60 -15

No files matched your search

+21 -6
View File
@@ -804,11 +804,16 @@ struct PostureItem {
/// can open, the hint names a chord you can press right now (`Esc to
/// interrupt`, `Enter again to send now`). An 80-column row carrying the
/// filesystem-scope notice cannot hold all of it, and losing the affordance
/// to keep the stopwatch is the wrong trade. The turn half goes before the
/// session half: the transcript's active row and the spinner also show the
/// turn is alive, while the session total is stated nowhere else. Above
/// them the context-cap warning, which is not a hint but the reason the
/// next turn will not start at all.
/// to keep the stopwatch is the wrong trade. When both halves would paint,
/// the turn half goes before the session half: the transcript's active row
/// and the spinner also show the turn is alive, while the session total is
/// stated nowhere else. When the session half is suppressed (#6041) or
/// otherwise absent, the turn half sheds at the session-clock rung instead
/// so the ladder does not abandon the only clock at the turn-only rung
/// while a both-clocks row would still be stating a stopwatch (#6084).
/// The hint and counts still outrank it (#5914). Above them the
/// context-cap warning, which is not a hint but the reason the next turn
/// will not start at all.
const SHED_TURN_CLOCK: u8 = 1;
const SHED_SESSION_CLOCK: u8 = 2;
const SHED_HINT: u8 = 3;
@@ -854,7 +859,17 @@ fn posture_items(footer: &TidelineFooter<'_>, shed: u8) -> Vec<PostureItem> {
count_index: None,
});
}
if let Some((clock, ink)) = footer.turn_clock.filter(|_| shed < SHED_TURN_CLOCK) {
// When no session half will paint, shed the turn clock at the session
// rung so a width that would keep the session half (and drop the turn)
// still keeps the only informative clock (#6084). Hint and counts still
// outrank it (#5914). With both halves present the turn half still goes
// first.
let turn_shed = if footer.session_clock.is_none() {
SHED_SESSION_CLOCK
} else {
SHED_TURN_CLOCK
};
if let Some((clock, ink)) = footer.turn_clock.filter(|_| shed < turn_shed) {
items.push(PostureItem {
text: footer.sym(clock),
ink,
@@ -640,6 +640,29 @@ fn a_session_that_has_not_worked_states_no_clock() {
);
}
/// #6084: when the session half will not paint, the shed ladder must not
/// drop the turn clock at the turn-only rung — that would leave no clock
/// at all. Find a width where both-clocks already sheds the turn half while
/// keeping the session half; with only the turn clock, that width must keep
/// it (shed at the session-clock rung instead).
#[test]
fn absent_session_clock_keeps_turn_past_the_turn_only_rung() {
let mut first_turn = working();
first_turn.session_clock = None;
let both = working();
let width = (8..=160u16)
.find(|&w| {
let text = draw(w, 3, &both.widget(&UI_THEME));
text.contains("worked 41m 12s") && !text.contains("working 1m 15s")
})
.expect("both-clocks fixture must shed turn before session at some width");
let text = draw(width, 3, &first_turn.widget(&UI_THEME));
assert!(
text.contains("working 1m 15s"),
"{width}: with no session half, the turn clock must survive the turn-only shed rung (#6084): {text}"
);
}
/// Narrow terminals shed both clock halves before the hint and the counts,
/// the turn half before the session half, and neither reading is ever
/// painted half-truncated.
+16 -9
View File
@@ -136,9 +136,12 @@ fn count_rows_containing(rows: &[String], needle: &str) -> usize {
/// frame: the context reading, the mode and permission chips, the model,
/// the cost, the agent count, and the help hint.
///
/// 160 columns joins the blocker sizes so the working clock's two halves
/// are asserted at a width that holds both; at 80 and 120 they shed by
/// design (#5914) and the row is asserted for what it does keep.
/// 160 columns joins the blocker sizes so both working-clock halves can
/// paint together when the session half is present; at 80 and 120 the
/// clocks shed by design (#5914) against the pinned scope notice. The
/// #6084 shed-order fix (sole turn clock uses the session rung) is pinned
/// in `phase_strip::tideline_tests`, where the narrower permission chip
/// exposes the width band the one-owner fixture's notice collapses.
#[test]
fn composed_frame_paints_each_fact_in_exactly_one_row() {
for (width, height) in [(80u16, 24u16), (120, 32), (160, 40)] {
@@ -207,14 +210,18 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() {
// The bar carries the working clock (#5914) — how long the current
// turn has been doing what it is doing, and how long the session has
// worked. Both halves shed before the hint and the counts, so a
// narrow row keeps the affordances and drops the stopwatch: the
// session half needs 120 columns here, the turn half 160. Each
// paints in exactly one row wherever it paints. The metrics line
// carries no repository, branch or provider.
// narrow row keeps the affordances and drops the stopwatch. When
// both would paint, the session half needs ~120 columns here and
// the turn half ~160; each paints in exactly one row wherever it
// paints. The metrics line carries no repository, branch or provider.
// First turn: the turn half names the phase and stays; the session
// reading is the identical duration, so it is suppressed rather than
// stated twice (#6041) — the turn half is the one that needs 160
// columns, so a narrower row keeps the affordances and no stopwatch.
// stated twice (#6041). With no session half to paint, the turn
// clock sheds at the session rung (#6084) rather than first — but
// against this fixture's pinned scope notice, turn+counts+hint is
// still just over a 120-column budget, so the hint wins here and
// the turn half needs ~160. The shed-order contract itself lives
// in tideline_tests.
let turn_needle = "sub-agents underway 1m 15s";
if width >= 160 {
assert!(rows[posture].contains(turn_needle), "{}", rows[posture]);