fix(tui): the footer's permission chip outranks the mode word (#5796)

* 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c

---------

Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-09-01 12:23:24 -07:00
committed by GitHub
parent 55b53b6c31
commit b26f53c873
3 changed files with 60 additions and 4 deletions

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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"
);
}