diff --git a/board/_board.py b/board/_board.py index 17590bf..eb147cf 100755 --- a/board/_board.py +++ b/board/_board.py @@ -613,6 +613,23 @@ def collect_devbox(): AUTONOMY_ROW_CAP = 5 +def _judgement_chip(value, cap=28): + """The log's two judgement cells, as a chip-sized label. + + The log writes them as ` ()` — "safe (feature medium cap; + same resumed acknowledged launch ...)" — because it is a record, read as a + table. A pill is not a table cell: it cannot wrap, so the whole sentence + became one chip a thousand pixels wide and the board scrolled sideways on + a phone. The verdict is the part that belongs on a chip; the why stays in + the log, which is where the row's link sends you. + + It also restores the colour: `_LEVEL_TONES`/`_OUTCOME_TONES` key off the + bare verdict, so every one of these rows was silently rendering neutral. + """ + head = value.split(" (", 1)[0].strip() + return head if len(head) <= cap else head[:cap - 1].rstrip() + "\u2026" + + def collect_autonomy(): """The tail of the Mind's autonomy calibration log — what ran unattended lately and how it ended (read verbatim; the log stays the record).""" @@ -623,7 +640,8 @@ def collect_autonomy(): r"^\|\s*(\d{4}-\d{2}-\d{2})\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|" r"[^|]*\|\s*([^|]+?)\s*\|", log.read_text(encoding="utf-8"), re.M) - return [{"date": d, "task": t[:70], "level": lvl, "outcome": o} + return [{"date": d, "task": t[:70], + "level": _judgement_chip(lvl), "outcome": _judgement_chip(o)} for d, t, lvl, o in rows[-AUTONOMY_ROW_CAP:]] diff --git a/board/_theme.py b/board/_theme.py index 2ffa22f..519cad6 100644 --- a/board/_theme.py +++ b/board/_theme.py @@ -381,9 +381,17 @@ def mark(key): /* --- facet pills: the backlog, scannable by colour --------------------- */ .facets{color:var(--muted);font-size:.85em} .tags{display:block;margin-top:.32rem;line-height:1.9} -.pill{display:inline-block;padding:.06em .5em;border-radius:999px; +/* A pill is a LABEL, and a label that will not fit is elided, never allowed + to push the page sideways. `nowrap` is what makes a chip read as a chip, so + it is also the one thing the page-wide wrap guard above cannot reach: an + over-long value (a board handing a whole log sentence to a facet) used to + run a chip a thousand pixels wide. The row's prose carries the meaning; the + chip carries the word. `vertical-align:bottom` because `overflow:hidden` + moves an inline-block's baseline to its bottom edge. */ +.pill{display:inline-block;max-width:100%%;padding:.06em .5em;border-radius:999px; font-size:.74em;font-weight:650;letter-spacing:.015em;white-space:nowrap; - vertical-align:.09em;border:1px solid var(--edge);background:var(--tint); + overflow:hidden;text-overflow:ellipsis; + vertical-align:bottom;border:1px solid var(--edge);background:var(--tint); color:var(--accent)} .pill+.pill{margin-left:.28rem} .pill.n{border-color:var(--line);background:var(--btn);color:var(--muted)} diff --git a/tests/test_board.py b/tests/test_board.py index cd0ae22..670d560 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -837,3 +837,31 @@ def test_missing_mind_degrades_honestly(tmp_path): assert any("community" in d for d in s["degraded"]) assert any("resume" in d for d in s["degraded"]) assert s["resume"]["tasks"] == [] + + +# ------------------------------------------------------- the autonomy chips -- + + +def test_a_judgement_cell_becomes_a_chip_sized_label(tmp_path): + """The log writes its two judgement columns as ` ()`, because + it is a record read as a table. A pill cannot wrap, so the whole sentence + used to render as one chip a thousand pixels wide — the board scrolled + sideways on a phone, and the tone lookups (which key off the bare verdict) + silently fell through to neutral on every such row.""" + log = AUTONOMY_LOG + ( + "| 2026-08-03 | third-task (#3) " + "| safe (feature medium cap; same resumed acknowledged launch and " + "campaign merge authorization) | tests pass " + "| merged-unchanged (RepoA#535 merge b9d9927f; issue left open) |\n" + "| 2026-08-04 | fourth-task (#4) " + "| human-authorized merge after accepted smoke exception and " + "independent review | tests pass | amended |\n") + stub = _fabricate(tmp_path, _default_fixtures()) + (tmp_path / "PyAutoMind" / "autonomy_log.md").write_text(log) + page = _run(["--html"], tmp_path, stub).stdout + # The verdict is the chip; the why stays in the log. + assert 'safe' in page + assert 'merged-unchanged' in page + assert "campaign merge authorization" not in page + # No parenthetical to cut? Then the head is elided, never left full length. + assert '"pill n">human-authorized merge afte…' in page diff --git a/tests/test_board_theme.py b/tests/test_board_theme.py index 4406f48..4279c72 100644 --- a/tests/test_board_theme.py +++ b/tests/test_board_theme.py @@ -206,3 +206,17 @@ def test_no_component_rule_re_declares_the_wrap(): css = re.sub(r"/\*.*?\*/", "", _theme.css("mind"), flags=re.S) body_rule = re.search(r"^body\{.*?\}", css, re.S | re.M).group(0) assert css.replace(body_rule, "").count("overflow-wrap") == 0 + + +def test_a_pill_is_bounded_because_nowrap_is_outside_the_wrap_guard(): + """`white-space:nowrap` is what makes a chip read as a chip, and it is the + one thing `body{overflow-wrap}` cannot reach. A board that hands a pill a + sentence must get an ellipsis, not a chip wider than the phone.""" + rule = re.search(r"^\.pill\{.*?\}", _theme.css("mind"), re.S | re.M).group(0) + flat = rule.replace("\n ", "") + assert "max-width:100%" in flat + assert "overflow:hidden" in flat + assert "text-overflow:ellipsis" in flat + # overflow:hidden moves an inline-block's baseline to its bottom edge, so + # the old optical nudge would drop every chip half a line. + assert "vertical-align:bottom" in flat