From b26f53c873fe8d635476ab01444f7d18581789e8 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 1 Sep 2026 12:23:24 -0700 Subject: [PATCH] fix(tui): the footer's permission chip outranks the mode word (#5796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(tui): the footer's permission chip outranks the mode word Founder-reported: the "full access" posture chip was missing from the footer in operate mode. The merged footer paints the posture lockup left-to-right and drops any chip that would run past the left block's edge, so the *last* chip was always the first casualty — and the order was [mode, permission]. Operate mode made it reproducible rather than occasional: `operate` is the longest of the three mode words, so it consumed the cells `full access` needed and the footer silently stopped reporting that the session could write anywhere. A preference word must not outbid a safety fact. When only one chip fits, permission keeps the cells; painting order stays mode-then-permission so the lockup does not shift when both fit. Regression test sweeps widths 40..=120 and asserts no width keeps the mode word while shedding permission. Without the fix it fails at width 69 — an ordinary split-pane width, not a degenerate one. tui footer_: 33 passed; 0 failed (existing posture and golden tests included, no golden drift). cargo fmt --check clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c * docs(agents): code first, then tests Founder rule, 2026-09-01. The implementation is written and proven to run first; tests are added afterward to cover what was actually built. No TDD here, and this overrides any skill or default that mandates it (superpowers `test-driven-development` in particular). An existing test that only encodes old behavior is evidence, not a veto. Placed beside the existing selective-evidence policy it extends, and scoped so it does not cancel the "Claiming a test passed" rule: a regression test written after a fix still has to be shown failing without it. That rule is what makes an after-the-fact test worth keeping, and it is the only part of test-first discipline that was ever earning its cost. No conflicting TDD mandate exists in the scoped crate guidance (checked crates/*/AGENTS.md and docs/). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c --------- Co-authored-by: CodeWhale Bot Co-authored-by: Claude Opus 5 (1M context) --- AGENTS.md | 9 +++++ crates/tui/src/tui/phase_strip.rs | 19 +++++++--- .../tui/src/tui/phase_strip/tideline_tests.rs | 36 +++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c6237148..e4ef6265f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,6 +149,15 @@ because they did anything wrong. Treat their time as more expensive than ours. - Product intent and observed runtime behavior outrank a test's preferred implementation shape. Fix the product; do not contort production code to preserve a brittle assertion. +- Code first, then tests. Write the implementation and prove it runs, then add + or adjust tests to cover what was actually built. Never write tests first and + never practice TDD here — this overrides any skill or default that mandates + it, including superpowers `test-driven-development`. Tests stay the gate + before a push; they are not the design driver. An existing test that only + encodes old behavior is evidence, not a veto: change it with the code rather + than bending the code to keep it green. This does not relax the rule under + "Claiming a test passed" — a regression test written *after* the fix still + has to be shown failing without it. - Tests are selective evidence, not the specification. Do not add tests by default. Add or retain one when it cheaply protects a high-risk behavior such as safety, data integrity, protocol compatibility, or a reproduced regression. diff --git a/crates/tui/src/tui/phase_strip.rs b/crates/tui/src/tui/phase_strip.rs index 852cc8aed..50ae16f1b 100644 --- a/crates/tui/src/tui/phase_strip.rs +++ b/crates/tui/src/tui/phase_strip.rs @@ -866,10 +866,21 @@ pub fn render_tideline_footer(area: Rect, buf: &mut Buffer, footer: &TidelineFoo } // Posture chips after the cost, each fitting whole or standing down — // a clipped posture word is worse than none (the classic header's rule). - for chip in [footer.mode_chip, footer.permission_chip] - .into_iter() - .flatten() - { + // + // When only one of the two can fit, permission keeps the cells. The mode + // word is a preference (`act` / `plan` / `operate`); the permission + // phrase is a safety fact, and a silently shed `full access` is the + // footer under-reporting the authority the session actually holds. This + // bit most in operate mode, whose mode word is the longest of the three + // and so consumed the budget the permission chip needed. Painting order + // stays mode-then-permission, so the lockup does not shift when both fit. + let chip_cells = |text: &str| ITEM_SEPARATOR_WIDTH + footer.sym(text).width(); + let mut mode_chip = footer.mode_chip; + if let (Some(mode), Some(permission)) = (footer.mode_chip, footer.permission_chip) { + let both = chip_cells(mode.0) + chip_cells(permission.0); + mode_chip = (usize::from(x) + both <= usize::from(left_edge_end)).then_some(mode); + } + for chip in [mode_chip, footer.permission_chip].into_iter().flatten() { let text = footer.sym(chip.0); let needs = ITEM_SEPARATOR_WIDTH + text.width(); if x + needs as u16 <= left_edge_end { diff --git a/crates/tui/src/tui/phase_strip/tideline_tests.rs b/crates/tui/src/tui/phase_strip/tideline_tests.rs index 9c499ee63..654f5b90b 100644 --- a/crates/tui/src/tui/phase_strip/tideline_tests.rs +++ b/crates/tui/src/tui/phase_strip/tideline_tests.rs @@ -242,3 +242,39 @@ fn footer_degenerate_sizes_do_not_panic() { let _ = draw(w, h, &footer); } } + +/// Permission outranks mode when only one posture chip fits. +/// +/// The footer paints the lockup left-to-right and drops whatever runs past +/// the left block's edge, so the *last* chip was the first casualty — and +/// that was permission. Operate mode made it reproducible: `operate` is the +/// longest of the three mode words, so it ate the cells `full access` +/// needed and the footer stopped reporting that the session could write +/// anywhere. A preference word must never outbid a safety fact. +#[test] +fn footer_permission_chip_outranks_mode_when_only_one_fits() { + let mut fixture = thinking_footer(); + fixture.mode_chip = Some(("operate", ChromeInk::PolicyAct)); + fixture.permission_chip = Some(("full access", ChromeInk::PermissionFullAccess)); + + // Sweep the widths between "neither fits" and "both fit". Wherever a + // single chip fits, it has to be the permission phrase. + let mut saw_permission_alone = false; + for width in 40..=120u16 { + let text = draw(width, 12, &fixture.widget(&UI_THEME)); + let has_mode = text.contains("operate"); + let has_permission = text.contains("full access"); + assert!( + !(has_mode && !has_permission), + "width {width} kept the mode word and shed the permission phrase: {text}" + ); + if has_permission && !has_mode { + saw_permission_alone = true; + } + } + assert!( + saw_permission_alone, + "no width in 40..=120 shed the mode word while keeping permission — \ + the sweep no longer covers the contested band" + ); +}