From ea3c8633ff8acf7815b60bad5af5e75666e69dfe Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 18:13:23 -0400 Subject: [PATCH 1/3] Skip sender geometry admission when committed damage is empty --- src/textual/_compositor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index bfe113cf3f..65535b5159 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -1650,6 +1650,8 @@ def mutation_box(self, widget: Widget) -> BoxModel | None: def pending_for(self, widget: Widget) -> bool: """Whether this sender still owns damage in the original committed scene.""" + if not self._dirty_regions: + return False regions = widget.screen._compositor.deferred_regions((widget,)) return any(damage.overlaps(region) for damage in self._dirty_regions for region in regions) From 6c5c0a055d3ae2eb881858ea2e97228eda97bebe Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 18:59:15 -0400 Subject: [PATCH 2/3] Supply focus dependency rules from the existing stylesheet index --- .../focus-rule-supply-20261006/before.json | 1014 +++++++++++++++++ src/textual/css/stylesheet.py | 14 +- tests/test_focus_declaration_targets.py | 51 + 3 files changed, 1076 insertions(+), 3 deletions(-) create mode 100644 evidence/focus-rule-supply-20261006/before.json diff --git a/evidence/focus-rule-supply-20261006/before.json b/evidence/focus-rule-supply-20261006/before.json new file mode 100644 index 0000000000..cdd955903f --- /dev/null +++ b/evidence/focus-rule-supply-20261006/before.json @@ -0,0 +1,1014 @@ +[ + { + "repo": "/home/ts/wt/textual-frame-publication-20261006", + "revision": "ea3c8633ff8acf7815b60bad5af5e75666e69dfe", + "root": "src/textual", + "parsed": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_parser.py", + 115, + "declare parse" + ], + [ + "src/textual/_xterm_parser.py", + 145, + "declare parse" + ], + [ + "src/textual/actions.py", + 26, + "declare parse" + ], + [ + "src/textual/app.py", + 1627, + "self.pseudo_classes", + "Load" + ], + [ + "src/textual/app.py", + 4423, + "self.stylesheet.update_app_focus", + "Load" + ], + [ + "src/textual/color.py", + 527, + "declare parse" + ], + [ + "src/textual/css/_styles_builder.py", + 331, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 563, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 586, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 340, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 345, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 475, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 503, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 559, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 582, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 637, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 778, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 804, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 824, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 838, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 852, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 857, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 865, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 928, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 951, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 952, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1061, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1271, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 660, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 673, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 686, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 697, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 763, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 966, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 978, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 997, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1017, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1018, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1031, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1043, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1078, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1096, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1097, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1122, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1123, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1150, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1184, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1196, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1206, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1282, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 147, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 260, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 497, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1104, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1109, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1130, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1135, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1163, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1164, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1169, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1170, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 232, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 292, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 355, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 377, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 399, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 711, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/_styles_builder.py", + 1290, + "self.styles._rules", + "Load" + ], + [ + "src/textual/css/model.py", + 352, + "declare _post_parse" + ], + [ + "src/textual/css/model.py", + 362, + "self.pseudo_classes", + "Load" + ], + [ + "src/textual/css/model.py", + 150, + "self.pseudo_classes", + "Load" + ], + [ + "src/textual/css/model.py", + 165, + "self.pseudo_classes", + "Load" + ], + [ + "src/textual/css/model.py", + 164, + "self.pseudo_classes", + "Load" + ], + [ + "src/textual/css/model.py", + 366, + "selector.pseudo_classes", + "Load" + ], + [ + "src/textual/css/model.py", + 134, + "self.pseudo_classes", + "Load" + ], + [ + "src/textual/css/model.py", + 258, + "selector.pseudo_classes", + "Load" + ], + [ + "src/textual/css/parse.py", + 452, + "declare parse" + ], + [ + "src/textual/css/parse.py", + 233, + "final_selector.pseudo_classes", + "Load" + ], + [ + "src/textual/css/parse.py", + 234, + "nested_selector.pseudo_classes", + "Load" + ], + [ + "src/textual/css/scalar.py", + 244, + "declare parse" + ], + [ + "src/textual/css/styles.py", + 782, + "declare parse" + ], + [ + "src/textual/css/styles.py", + 924, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 925, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1077, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1628, + "self._base_styles._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1628, + "self._inline_styles._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 984, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 986, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1007, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1466, + "self._base_styles._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1466, + "other._base_styles._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1467, + "self._inline_styles._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1467, + "other._inline_styles._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 971, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 973, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 1120, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 966, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 968, + "self._rules", + "Load" + ], + [ + "src/textual/css/styles.py", + 968, + "self._rules", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 212, + "declare rules_map" + ], + [ + "src/textual/css/stylesheet.py", + 407, + "declare parse" + ], + [ + "src/textual/css/stylesheet.py", + 463, + "declare reparse" + ], + [ + "src/textual/css/stylesheet.py", + 962, + "declare _update_focus_dependencies" + ], + [ + "src/textual/css/stylesheet.py", + 171, + "self._rules", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 172, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 176, + "self._require_parse", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 205, + "self._require_parse", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 209, + "self._rules", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 224, + "self._rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 341, + "self._require_parse", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 404, + "self._require_parse", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 405, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 448, + "self._rules", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 460, + "self._require_parse", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 461, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 568, + "self.rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 207, + "self._require_parse", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 208, + "self._rules", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 218, + "self._rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 223, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 492, + "self._rules", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 496, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 498, + "self._require_parse", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 625, + "self.rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 945, + "self._update_focus_dependencies", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 954, + "self._update_focus_dependencies", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 958, + "self._update_focus_dependencies", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 575, + "rule.pseudo_classes", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 838, + "base_styles._rules", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 844, + "base_styles._rules", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 980, + "selector.pseudo_classes", + "Load" + ], + [ + "src/textual/design.py", + 66, + "declare parse" + ], + [ + "src/textual/drivers/_byte_stream.py", + 119, + "declare parse" + ], + [ + "src/textual/drivers/_byte_stream.py", + 147, + "declare parse" + ], + [ + "src/textual/screen.py", + 1101, + "self.app.stylesheet.update_focus_within", + "Load" + ], + [ + "src/textual/style.py", + 318, + "declare parse" + ], + [ + "src/textual/widget.py", + 4322, + "self.app.stylesheet.update_widget_focus", + "Load" + ] + ] + }, + { + "repo": "/home/ts/wt/textual-frame-publication-20261006", + "revision": "ea3c8633ff8acf7815b60bad5af5e75666e69dfe", + "root": "tests", + "parsed": 467, + "omissions": [], + "sites": [ + [ + "tests/css/test_current_source_rules.py", + 85, + "sheet._rules", + "Load" + ], + [ + "tests/test_app.py", + 41, + "button.pseudo_classes", + "Load" + ], + [ + "tests/test_app.py", + 58, + "button.pseudo_classes", + "Load" + ], + [ + "tests/test_focus_declaration_targets.py", + 91, + "app.stylesheet.update_focus_within", + "Load" + ], + [ + "tests/test_parser.py", + 8, + "declare parse" + ], + [ + "tests/test_parser.py", + 37, + "declare parse" + ] + ] + }, + { + "repo": "/home/ts/wt/toad-sidebar-context-pointer-20261006", + "revision": "d7273c666ea04ff13c1a677bb5851637da273490", + "root": "src/toad", + "parsed": 288, + "omissions": [], + "sites": [ + [ + "src/toad/acp/log_records.py", + 18, + "declare parse" + ], + [ + "src/toad/acp/log_records.py", + 23, + "declare parse" + ], + [ + "src/toad/acp/log_records.py", + 29, + "declare parse" + ], + [ + "src/toad/ansi/_ansi.py", + 232, + "declare parse" + ], + [ + "src/toad/ansi/_ansi.py", + 627, + "declare parse" + ], + [ + "src/toad/ansi/_stream_parser.py", + 298, + "declare parse" + ], + [ + "src/toad/application_actions.py", + 55, + "declare parse" + ], + [ + "src/toad/application_actions.py", + 98, + "declare parse" + ], + [ + "src/toad/conversation_markdown.py", + 186, + "declare parse" + ], + [ + "src/toad/question_actions.py", + 83, + "declare parse" + ], + [ + "src/toad/settings.py", + 111, + "declare parse" + ], + [ + "src/toad/settings.py", + 271, + "declare parse" + ], + [ + "src/toad/settings.py", + 276, + "declare parse" + ], + [ + "src/toad/settings.py", + 293, + "declare parse" + ], + [ + "src/toad/settings.py", + 326, + "declare parse" + ], + [ + "src/toad/settings.py", + 334, + "declare parse" + ], + [ + "src/toad/settings.py", + 349, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 77, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 101, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 114, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 133, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 188, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 216, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 268, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 286, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 320, + "declare parse" + ], + [ + "src/toad/slash_command.py", + 344, + "declare parse" + ], + [ + "src/toad/target_commands.py", + 144, + "declare parse" + ], + [ + "src/toad/target_commands.py", + 192, + "declare parse" + ] + ] + } +] diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 7ae38d388e..c586e6909f 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -210,16 +210,19 @@ def rules(self) -> list[RuleSet]: @property def rules_map(self) -> dict[str, list[RuleSet]]: - """Structure that maps a selector on to a list of rules. + """Index target names and pseudo-class dependencies from parsed rules. Returns: Mapping of selector to rule sets. """ + rules = self.rules if self._rules_map is None: rules_map: dict[str, list[RuleSet]] = defaultdict(list) - for rule in self.rules: + for rule in rules: for name in rule.selector_names: rules_map[name].append(rule) + for pseudo_class in rule.pseudo_classes: + rules_map[f":{pseudo_class}"].append(rule) self._rules_map = dict(rules_map) return self._rules_map @@ -974,7 +977,12 @@ def _update_focus_dependencies( if not scopes: return scope_names: dict[DOMNode, set[str]] = {} - for rule in self.rules: + rules_map = self.rules_map + rules = dict.fromkeys( + rule for pseudo_class in pseudo_classes + for rule in rules_map.get(f":{pseudo_class}", ()) + ) + for rule in rules: for root, focus_node in scopes: if any( selector.pseudo_classes & pseudo_classes diff --git a/tests/test_focus_declaration_targets.py b/tests/test_focus_declaration_targets.py index cc5ea0d93f..0298e1e0de 100644 --- a/tests/test_focus_declaration_targets.py +++ b/tests/test_focus_declaration_targets.py @@ -98,6 +98,57 @@ def compose(self): assert inner_target.styles.color == Color.parse("green") +async def test_focus_dependency_supply_follows_read_and_reparse(tmp_path): + class Badge(Label): + COMPONENT_CLASSES = {"badge--label"} + + class FocusApp(App): + AUTO_FOCUS = "#outside" + CSS = """ + .detail, .badge--label { color: blue; } + """ + + def compose(self): + with VerticalGroup(id="host"): + yield Button("inside", id="inside") + yield Badge("target", classes="detail") + yield Button("outside", id="outside") + + app = FocusApp() + async with app.run_test() as pilot: + await pilot.pause() + badge = app.query_one(Badge) + # Acquire the original index before a new source is read. Both an + # immediate descendant and a virtual component must gain its dependency. + previous_index = app.stylesheet.rules_map + path = tmp_path / "focus.tcss" + path.write_text(""" + #host:focus-within > .detail { color: red; } + #host:focus-within .badge--label { color: red; } + """) + app.stylesheet.read(path) + app.query_one("#inside").focus() + await pilot.pause() + assert app.stylesheet.rules_map is not previous_index + assert badge.styles.color == Color.parse("red") + assert badge.get_component_styles("badge--label").color == Color.parse("red") + + path.write_text(""" + #host:focus-within > .detail { color: green; } + #host:focus-within .badge--label { color: green; } + """) + app.stylesheet.read(path) + app.stylesheet.reparse() + app.query_one("#outside").focus() + await pilot.pause() + assert badge.styles.color == Color.parse("blue") + assert badge.get_component_styles("badge--label").color == Color.parse("blue") + app.query_one("#inside").focus() + await pilot.pause() + assert badge.styles.color == Color.parse("green") + assert badge.get_component_styles("badge--label").color == Color.parse("green") + + async def test_instant_style_changes_do_not_materialize_animation_rule_graphs(): app = App() async with app.run_test() as pilot: From f5ad535ca37d9e274209ba11cf70d0bce78d390e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:00:48 -0400 Subject: [PATCH 3/3] Record matched focus-supply App result and consumer closure --- evidence/focus-rule-supply-20261006/README.md | 11 ++ .../focus-rule-supply-20261006/RESULT.json | 44 ++++++++ .../focus-rule-supply-20261006/after.json | 103 ++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 evidence/focus-rule-supply-20261006/README.md create mode 100644 evidence/focus-rule-supply-20261006/RESULT.json create mode 100644 evidence/focus-rule-supply-20261006/after.json diff --git a/evidence/focus-rule-supply-20261006/README.md b/evidence/focus-rule-supply-20261006/README.md new file mode 100644 index 0000000000..022237e7cf --- /dev/null +++ b/evidence/focus-rule-supply-20261006/README.md @@ -0,0 +1,11 @@ +# Focus rule supply + +The existing RuleSet parser owns pseudo-class dependencies from every selector, including ancestor and nested selectors. Stylesheet.rules_map now indexes those declarations alongside target names. Focus/blur/focus-within acquisition selects indexed rules and deletes its scan of every rule for every changed scope. Actual dynamic selector checks, nested-scope target traversal, component/virtual DOM handling and cascade application remain unchanged. Candidate collection unions rule identities; cascade precedence still comes from the unchanged apply path. + +The rules_map reader first acquires rules, so a pending read/parse cannot serve an older index. Successful parse and reparse retire the same existing index; copies retain their normal source/parse lifetime. New pseudo-class keys cannot collide with native target keys (type, *, #id, .class). No new cache or registry. + +AST before: 249 native production, 467 test and 288 Toad production modules, zero omissions. RuleSet._post_parse is the dependency producer; Stylesheet.rules_map is the one index; App application focus, Widget focus and Screen changed focus-within scopes converge on _update_focus_dependencies. Existing matching/index readers were inspected. After native parser: 249 modules, zero omissions. External dynamic mutation is not proven absent by lexical AST; no new support for post-parse selector mutation is claimed. Three production lines replaced; index lifetime and all three focus consumers use the existing owner. + +Seven focused controls passed, including real mounted descendants/components across CSS read and reparse. One original matched sidebar App finished zero with empty stderr, 508 widgets/10 tabs, no provider. Right profile: same three focus updates, own declaration discovery 7.05ms -> .47ms and full method 19.03ms -> 11.22ms. Left full method 18.51ms -> 7.99ms. This is a single source headless observation. First-display medians 41.8/42.2ms versus 40.1/45.6ms, tails vary, paint was slower: no reliable overall frame or live speedup claim. + +RESULT.json binds original raw profiles/logs and source evidence. No new build, install, provider, public operation or repeated benchmark. The existing empty-damage pending_for checkpoint remains on this same PR, independently scoped. diff --git a/evidence/focus-rule-supply-20261006/RESULT.json b/evidence/focus-rule-supply-20261006/RESULT.json new file mode 100644 index 0000000000..4606f7bf6e --- /dev/null +++ b/evidence/focus-rule-supply-20261006/RESULT.json @@ -0,0 +1,44 @@ +{ + "source": "6c5c0a055", + "controls": "7 passed in 1.09s", + "app_exit": 0, + "app_widgets": 508, + "app_tabs": 10, + "providers": 0, + "metrics": { + "before": { + "channels": { + "calls": 3, + "own_seconds": 0.006711723, + "inclusive_seconds": 0.018510871 + }, + "thread": { + "calls": 3, + "own_seconds": 0.007046747000000001, + "inclusive_seconds": 0.019031289 + } + }, + "after": { + "channels": { + "calls": 3, + "own_seconds": 0.00039156600000000004, + "inclusive_seconds": 0.007986836 + }, + "thread": { + "calls": 3, + "own_seconds": 0.00046633400000000005, + "inclusive_seconds": 0.011223700000000001 + } + } + }, + "scope": "Single source App completed headless first-display check, not installed/live/terminal-pixel latency. Whole-frame medians and tails vary; no reliable overall speedup.", + "keepers": { + "evidence/focus-rule-supply-20261006/before.json": "66889e3b9ba8f9bcb5b50e9c5d9bdfd0114f0b83a43897a89287bf99891d77bf", + "evidence/focus-rule-supply-20261006/after.json": "554f279fe1d0248212feea3dcbf0d3f69bb0ac2276fcdbd635676319dfd9312e", + "evidence/focus-rule-supply-20261006/controls.log": "c928e2b325450f4fe53ea38feef281d6c7aed611956284bd9706a12f60e5c96a", + "/home/ts/.cache/agent-scratch/focus-rule-supply-20261006/app.stdout.log": "5247e1d6a489d95bcead12039f0c6af7ada02fe1327cdf371d94b8a1a02f3c2d", + "/home/ts/.cache/agent-scratch/focus-rule-supply-20261006/app.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.cache/agent-scratch/focus-rule-supply-20261006/toggle-channels.pstats": "187497551c23b99916e7083098982a659112bdf7dcba9f7ea08ea63fff617fc6", + "/home/ts/.cache/agent-scratch/focus-rule-supply-20261006/toggle-thread.pstats": "762601eab880b54be284c0412aab7b5787a8b40bcc284a4b9af5962401140b89" + } +} diff --git a/evidence/focus-rule-supply-20261006/after.json b/evidence/focus-rule-supply-20261006/after.json new file mode 100644 index 0000000000..e04d3d55c6 --- /dev/null +++ b/evidence/focus-rule-supply-20261006/after.json @@ -0,0 +1,103 @@ +{ + "revision": "6c5c0a055d3ae2eb881858ea2e97228eda97bebe", + "parsed": 249, + "omissions": [], + "sites": [ + [ + "src/textual/app.py", + 4423, + "self.stylesheet.update_app_focus", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 172, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 227, + "self._rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 408, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 464, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 571, + "self.rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 980, + "self.rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 219, + "self._rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 226, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 499, + "self._rules_map", + "Store" + ], + [ + "src/textual/css/stylesheet.py", + 628, + "self.rules_map", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 948, + "self._update_focus_dependencies", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 957, + "self._update_focus_dependencies", + "Load" + ], + [ + "src/textual/css/stylesheet.py", + 961, + "self._update_focus_dependencies", + "Load" + ], + [ + "src/textual/screen.py", + 1101, + "self.app.stylesheet.update_focus_within", + "Load" + ], + [ + "src/textual/widget.py", + 4322, + "self.app.stylesheet.update_widget_focus", + "Load" + ] + ] +}