aru-code 0.41.0__tar.gz → 0.42.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. {aru_code-0.41.0/aru_code.egg-info → aru_code-0.42.0}/PKG-INFO +1 -1
  2. aru_code-0.42.0/aru/__init__.py +1 -0
  3. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/app.py +18 -0
  4. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/chat.py +147 -5
  5. {aru_code-0.41.0 → aru_code-0.42.0/aru_code.egg-info}/PKG-INFO +1 -1
  6. {aru_code-0.41.0 → aru_code-0.42.0}/pyproject.toml +1 -1
  7. aru_code-0.41.0/aru/__init__.py +0 -1
  8. {aru_code-0.41.0 → aru_code-0.42.0}/LICENSE +0 -0
  9. {aru_code-0.41.0 → aru_code-0.42.0}/README.md +0 -0
  10. {aru_code-0.41.0 → aru_code-0.42.0}/aru/agent_factory.py +0 -0
  11. {aru_code-0.41.0 → aru_code-0.42.0}/aru/agents/__init__.py +0 -0
  12. {aru_code-0.41.0 → aru_code-0.42.0}/aru/agents/base.py +0 -0
  13. {aru_code-0.41.0 → aru_code-0.42.0}/aru/agents/catalog.py +0 -0
  14. {aru_code-0.41.0 → aru_code-0.42.0}/aru/agents/planner.py +0 -0
  15. {aru_code-0.41.0 → aru_code-0.42.0}/aru/cache_patch.py +0 -0
  16. {aru_code-0.41.0 → aru_code-0.42.0}/aru/checkpoints.py +0 -0
  17. {aru_code-0.41.0 → aru_code-0.42.0}/aru/cli.py +0 -0
  18. {aru_code-0.41.0 → aru_code-0.42.0}/aru/commands.py +0 -0
  19. {aru_code-0.41.0 → aru_code-0.42.0}/aru/completers.py +0 -0
  20. {aru_code-0.41.0 → aru_code-0.42.0}/aru/config.py +0 -0
  21. {aru_code-0.41.0 → aru_code-0.42.0}/aru/context.py +0 -0
  22. {aru_code-0.41.0 → aru_code-0.42.0}/aru/display.py +0 -0
  23. {aru_code-0.41.0 → aru_code-0.42.0}/aru/events.py +0 -0
  24. {aru_code-0.41.0 → aru_code-0.42.0}/aru/format/__init__.py +0 -0
  25. {aru_code-0.41.0 → aru_code-0.42.0}/aru/format/manager.py +0 -0
  26. {aru_code-0.41.0 → aru_code-0.42.0}/aru/format/runner.py +0 -0
  27. {aru_code-0.41.0 → aru_code-0.42.0}/aru/history_blocks.py +0 -0
  28. {aru_code-0.41.0 → aru_code-0.42.0}/aru/lsp/__init__.py +0 -0
  29. {aru_code-0.41.0 → aru_code-0.42.0}/aru/lsp/client.py +0 -0
  30. {aru_code-0.41.0 → aru_code-0.42.0}/aru/lsp/manager.py +0 -0
  31. {aru_code-0.41.0 → aru_code-0.42.0}/aru/lsp/protocol.py +0 -0
  32. {aru_code-0.41.0 → aru_code-0.42.0}/aru/memory/__init__.py +0 -0
  33. {aru_code-0.41.0 → aru_code-0.42.0}/aru/memory/extractor.py +0 -0
  34. {aru_code-0.41.0 → aru_code-0.42.0}/aru/memory/loader.py +0 -0
  35. {aru_code-0.41.0 → aru_code-0.42.0}/aru/memory/store.py +0 -0
  36. {aru_code-0.41.0 → aru_code-0.42.0}/aru/permissions.py +0 -0
  37. {aru_code-0.41.0 → aru_code-0.42.0}/aru/plugin_cache.py +0 -0
  38. {aru_code-0.41.0 → aru_code-0.42.0}/aru/plugins/__init__.py +0 -0
  39. {aru_code-0.41.0 → aru_code-0.42.0}/aru/plugins/custom_tools.py +0 -0
  40. {aru_code-0.41.0 → aru_code-0.42.0}/aru/plugins/hooks.py +0 -0
  41. {aru_code-0.41.0 → aru_code-0.42.0}/aru/plugins/manager.py +0 -0
  42. {aru_code-0.41.0 → aru_code-0.42.0}/aru/plugins/tool_api.py +0 -0
  43. {aru_code-0.41.0 → aru_code-0.42.0}/aru/providers.py +0 -0
  44. {aru_code-0.41.0 → aru_code-0.42.0}/aru/runner.py +0 -0
  45. {aru_code-0.41.0 → aru_code-0.42.0}/aru/runtime.py +0 -0
  46. {aru_code-0.41.0 → aru_code-0.42.0}/aru/select.py +0 -0
  47. {aru_code-0.41.0 → aru_code-0.42.0}/aru/session.py +0 -0
  48. {aru_code-0.41.0 → aru_code-0.42.0}/aru/sinks.py +0 -0
  49. {aru_code-0.41.0 → aru_code-0.42.0}/aru/streaming.py +0 -0
  50. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tool_policy.py +0 -0
  51. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/__init__.py +0 -0
  52. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/_diff.py +0 -0
  53. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/_shared.py +0 -0
  54. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/apply_patch.py +0 -0
  55. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/apply_patch_prompt.txt +0 -0
  56. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/ast_tools.py +0 -0
  57. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/codebase.py +0 -0
  58. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/delegate.py +0 -0
  59. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/delegate_prompt.txt +0 -0
  60. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/file_ops.py +0 -0
  61. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/gitignore.py +0 -0
  62. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/lsp.py +0 -0
  63. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/mcp_client.py +0 -0
  64. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/memory_tool.py +0 -0
  65. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/plan_mode.py +0 -0
  66. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/ranker.py +0 -0
  67. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/registry.py +0 -0
  68. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/search.py +0 -0
  69. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/shell.py +0 -0
  70. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/skill.py +0 -0
  71. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/tasklist.py +0 -0
  72. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/web.py +0 -0
  73. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tools/worktree.py +0 -0
  74. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/__init__.py +0 -0
  75. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/screens/__init__.py +0 -0
  76. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/screens/choice.py +0 -0
  77. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/screens/confirm.py +0 -0
  78. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/screens/search.py +0 -0
  79. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/screens/text_input.py +0 -0
  80. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/sinks.py +0 -0
  81. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/slash_bridge.py +0 -0
  82. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/ui.py +0 -0
  83. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/__init__.py +0 -0
  84. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/completer.py +0 -0
  85. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/context_pane.py +0 -0
  86. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/header.py +0 -0
  87. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/inline_choice.py +0 -0
  88. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/loaded_pane.py +0 -0
  89. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/status.py +0 -0
  90. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/thinking.py +0 -0
  91. {aru_code-0.41.0 → aru_code-0.42.0}/aru/tui/widgets/tools.py +0 -0
  92. {aru_code-0.41.0 → aru_code-0.42.0}/aru/ui.py +0 -0
  93. {aru_code-0.41.0 → aru_code-0.42.0}/aru_code.egg-info/SOURCES.txt +0 -0
  94. {aru_code-0.41.0 → aru_code-0.42.0}/aru_code.egg-info/dependency_links.txt +0 -0
  95. {aru_code-0.41.0 → aru_code-0.42.0}/aru_code.egg-info/entry_points.txt +0 -0
  96. {aru_code-0.41.0 → aru_code-0.42.0}/aru_code.egg-info/requires.txt +0 -0
  97. {aru_code-0.41.0 → aru_code-0.42.0}/aru_code.egg-info/top_level.txt +0 -0
  98. {aru_code-0.41.0 → aru_code-0.42.0}/setup.cfg +0 -0
  99. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_agents_base.py +0 -0
  100. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_agents_md_coverage.py +0 -0
  101. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_apply_patch.py +0 -0
  102. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_async_tool_permission.py +0 -0
  103. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cache_patch_metrics.py +0 -0
  104. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cache_patch_stop_reason.py +0 -0
  105. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_catalog.py +0 -0
  106. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_chat_scrollable.py +0 -0
  107. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_checkpoints.py +0 -0
  108. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli.py +0 -0
  109. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_advanced.py +0 -0
  110. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_base.py +0 -0
  111. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_completers.py +0 -0
  112. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_new.py +0 -0
  113. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_run_cli.py +0 -0
  114. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_session.py +0 -0
  115. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cli_shell.py +0 -0
  116. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_codebase.py +0 -0
  117. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_confabulation_regression.py +0 -0
  118. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_config.py +0 -0
  119. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_context.py +0 -0
  120. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_context_pane.py +0 -0
  121. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_cwd_awareness.py +0 -0
  122. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_delegate.py +0 -0
  123. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_events_backward_compat.py +0 -0
  124. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_events_schema.py +0 -0
  125. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_fork_ctx_concurrency.py +0 -0
  126. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_format.py +0 -0
  127. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_gitignore.py +0 -0
  128. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_guardrails_scenarios.py +0 -0
  129. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_invoke_skill.py +0 -0
  130. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_invoked_skills.py +0 -0
  131. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_loaded_pane_path.py +0 -0
  132. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_lsp.py +0 -0
  133. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_lsp_rename.py +0 -0
  134. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_main.py +0 -0
  135. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_markdown_to_text.py +0 -0
  136. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_mcp_client.py +0 -0
  137. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_mcp_health.py +0 -0
  138. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_memory.py +0 -0
  139. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_memory_tool.py +0 -0
  140. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_microcompact.py +0 -0
  141. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_permissions.py +0 -0
  142. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_plan_mode_refactor.py +0 -0
  143. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_plugin_cache.py +0 -0
  144. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_plugin_errors.py +0 -0
  145. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_plugin_hooks_v2.py +0 -0
  146. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_plugins.py +0 -0
  147. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_providers.py +0 -0
  148. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_ranker.py +0 -0
  149. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_reasoning.py +0 -0
  150. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_runner_interrupt.py +0 -0
  151. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_runner_recovery.py +0 -0
  152. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_runtime.py +0 -0
  153. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_select.py +0 -0
  154. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_skill_disallowed_tools.py +0 -0
  155. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_status_breakdown.py +0 -0
  156. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_status_cost.py +0 -0
  157. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_streaming_sink.py +0 -0
  158. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tasklist.py +0 -0
  159. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_thread_tool_timeout.py +0 -0
  160. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tool_policy.py +0 -0
  161. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_truncation_marker.py +0 -0
  162. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_app_boot.py +0 -0
  163. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_bindings.py +0 -0
  164. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_bus_flow.py +0 -0
  165. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_chat.py +0 -0
  166. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_chat_adversarial.py +0 -0
  167. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_completer.py +0 -0
  168. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_completer_dynamic.py +0 -0
  169. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_copy.py +0 -0
  170. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_input_behaviour.py +0 -0
  171. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_mention_expand.py +0 -0
  172. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_modals.py +0 -0
  173. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_mode_cycle.py +0 -0
  174. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_native_selection.py +0 -0
  175. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_permission_flow.py +0 -0
  176. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_plan_task_render.py +0 -0
  177. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_sidebar_toggle.py +0 -0
  178. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_slash_bridge.py +0 -0
  179. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_snapshot_smoke.py +0 -0
  180. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_thinking_and_boot.py +0 -0
  181. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_tui_widgets_visual.py +0 -0
  182. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_ui_adapter.py +0 -0
  183. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_worktree.py +0 -0
  184. {aru_code-0.41.0 → aru_code-0.42.0}/tests/test_worktree_session_restore.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.41.0
3
+ Version: 0.42.0
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.42.0"
@@ -1068,6 +1068,24 @@ class AruApp(App):
1068
1068
  self.query_one(ContextPane).refresh_from_session()
1069
1069
  except Exception:
1070
1070
  pass
1071
+ # Layer 9 self-heal — re-assert Textual's mouse-tracking
1072
+ # sequences at every turn boundary. If any content path
1073
+ # emitted a rogue DEC private-mode escape during the turn
1074
+ # (``\x1b[?1000l`` or similar) the terminal would have
1075
+ # silently disabled wheel reporting for every scroll area
1076
+ # in the app, with no way for us to detect it. Calling the
1077
+ # driver's enable-mouse path writes four short SGR sequences
1078
+ # (``?1000h`` / ``?1003h`` / ``?1015h`` / ``?1006h``) and
1079
+ # restores wheel input no matter what corrupted the state.
1080
+ # Idempotent when mouse tracking was never disabled. See the
1081
+ # Layer 9 post-mortem at the top of
1082
+ # ``aru/tui/widgets/chat.py`` for the full analysis.
1083
+ try:
1084
+ driver = self._driver
1085
+ if driver is not None:
1086
+ driver._enable_mouse_support()
1087
+ except Exception:
1088
+ pass
1071
1089
 
1072
1090
  # ── Bus wiring — ToolsPane + StatusPane subscribe to plugin events ──
1073
1091
 
@@ -128,6 +128,91 @@ cost and layout cost on the Aru side. This bug is about the
128
128
  terminal's own private-mode state being corrupted from outside —
129
129
  entirely unrelated to how fast we render, and invisible to any
130
130
  latency benchmark. Treat as a seventh layer: output-hygiene.
131
+
132
+ ----
133
+
134
+ Post-mortem — "mouse wheel dead during heavy streaming" (2026-04-23,
135
+ ``fix/scroll-refinement``)
136
+ ---------------------------------------------------------------------
137
+ **Symptom:** mouse wheel over the ChatPane does nothing while the
138
+ agent is actively streaming / running tool batches. TAB to focus
139
+ the pane + arrow keys / PgUp / PgDn works fine. Asymmetric enough
140
+ that it felt like "the mouse lost focus" — not a freeze.
141
+
142
+ Reported against session ``final-fantasy-battle/.aru/sessions/
143
+ e9397dc3.json``.
144
+
145
+ **Original (incorrect) theory:** interaction between
146
+ ``self.anchor()`` (layer 4) and ``_scroll_up_for_pointer``. This
147
+ was based on a misreading of Textual's source — see the Layer 9
148
+ correction below. ``_scroll_up_for_pointer`` does *not* pass
149
+ ``release_anchor=False``; it defaults to ``True`` (``widget.py:3378``
150
+ → ``widget.py:2730``), so wheel-up already releases the anchor via
151
+ the framework. The ``on_mouse_scroll_up`` handler we added
152
+ (``ChatPane.on_mouse_scroll_up``) is therefore redundant with the
153
+ framework's own behaviour — a no-op on the happy path. It is kept
154
+ as defensive redundancy because removing it is the same shape of
155
+ change as keeping it, but it should not be credited for "fixing"
156
+ anything.
157
+
158
+ **What the bug probably was:** the same Layer-7 class of issue
159
+ that the next session surfaced again — a rogue DEC private-mode
160
+ escape reaching the terminal and disabling X10 mouse reporting.
161
+ See Layer 9 for the real signature and the robust fix.
162
+
163
+ ----
164
+
165
+ Post-mortem — "wheel globally dead at end of stream" (2026-04-24,
166
+ ``fix/scroll-refinement`` continued)
167
+ ---------------------------------------------------------------------
168
+ **Symptom:** immediately after a long streaming turn concluded,
169
+ mouse wheel stopped working on *every* scrollable surface in the
170
+ app — ChatPane, sidebars, modals — simultaneously. TAB to walk
171
+ focus into a scrollbar and arrow-key scrolling from there worked.
172
+ Classic Layer-7 fingerprint: terminal-level mouse reporting got
173
+ turned off.
174
+
175
+ Reported against session ``final-fantasy-battle3/.aru/sessions/
176
+ 7e9e4549.json``: one mega-turn with 120 tool calls interleaved
177
+ with 66 text blocks, 31 plan-panel mounts via
178
+ ``add_renderable(scrollable=True)``, ~245 widgets in the pane.
179
+
180
+ **What we could prove:** a byte-level scan of the saved session
181
+ for C0 control chars turned up zero ``\\x1b`` bytes. The leak is
182
+ either (a) from a path that isn't persisted to ``session.json``
183
+ (tool ``stdout``/``stderr`` never reaches the chat directly but
184
+ transient UI strings, skill output, or reasoning tokens might),
185
+ or (b) a Windows ConPTY quirk during high-volume redraw where the
186
+ driver's mouse-enable state drops without us emitting anything
187
+ hostile. Chasing the exact source is caça ao fantasma; the
188
+ mitigation is structural.
189
+
190
+ **Two-prong fix:**
191
+
192
+ 1. **Close the last unsanitised content path —
193
+ ``_SanitizedRenderable``.** ``ChatMessageWidget`` already
194
+ sanitises everything that goes through its ``buffer``. Arbitrary
195
+ Rich renderables handed to ``add_renderable`` (plan panels, task
196
+ lists, diff previews, the logo) bypass that path and mount as
197
+ ``Static(renderable)``. The wrapper sits between the renderable
198
+ and Rich's console, filtering C0 bytes out of every segment's
199
+ ``.text`` before it reaches Textual's compositor. Matches the
200
+ Layer 7 sanitisation boundary for the unchecked route.
201
+
202
+ 2. **Self-heal at turn boundary —
203
+ ``AruApp._run_turn`` finally clause.** Call the driver's
204
+ ``_enable_mouse_support()`` after each turn finishes. That
205
+ re-emits Textual's own four mouse-enable sequences (``?1000h``,
206
+ ``?1003h``, ``?1015h``, ``?1006h`` — see
207
+ ``textual/drivers/windows_driver.py:56``). Cost is four short
208
+ writes; benefit is full recovery of wheel input regardless of
209
+ what corrupted the terminal state mid-turn. Idempotent: a no-op
210
+ when mouse tracking was never disabled.
211
+
212
+ Treat as a ninth layer: defence-in-depth against terminal-state
213
+ corruption. Prong 1 plugs the last known-possible leak inside our
214
+ code; prong 2 recovers even if something outside our reach drops
215
+ the state anyway.
131
216
  """
132
217
 
133
218
  from __future__ import annotations
@@ -182,6 +267,39 @@ def _sanitize_for_terminal(raw: str) -> str:
182
267
  return raw.translate(_CTRL_CHAR_TRANSLATION)
183
268
 
184
269
 
270
+ class _SanitizedRenderable:
271
+ """Wraps a Rich renderable so its output segments are stripped of C0 bytes.
272
+
273
+ ``_sanitize_for_terminal`` covers every string passing through
274
+ ``ChatMessageWidget.buffer``. Arbitrary Rich renderables handed to
275
+ ``ChatPane.add_renderable`` (plan panels, task lists, diff previews, the
276
+ startup logo) skip that widget and mount as a plain ``Static(renderable)``
277
+ — so a rogue ``\\x1b[?1000l`` inside a task description, a panel title, or
278
+ subprocess output echoed into a panel would flow straight through Rich
279
+ segments to Textual's compositor and onto the terminal, disabling mouse
280
+ tracking globally (Layer 7 signature).
281
+
282
+ This wrapper closes that gap: ``console.render`` yields segments from the
283
+ inner renderable, we strip C0 bytes from any segment whose ``.text``
284
+ contains them, and re-emit the cleaned stream. Rich's ``Segment`` is a
285
+ ``NamedTuple`` so ``seg._replace(text=...)`` is a cheap immutable swap.
286
+ Unchanged segments are re-emitted unmodified — the hot path is a single
287
+ ``str.translate`` on segment text which typically no-ops.
288
+ """
289
+
290
+ def __init__(self, inner: Any) -> None:
291
+ self._inner = inner
292
+
293
+ def __rich_console__(self, console: Any, options: Any) -> Any:
294
+ for seg in console.render(self._inner, options):
295
+ if seg.text:
296
+ clean = _sanitize_for_terminal(seg.text)
297
+ if clean != seg.text:
298
+ yield seg._replace(text=clean)
299
+ continue
300
+ yield seg
301
+
302
+
185
303
  def _scan_fences(text: str) -> tuple[int, int]:
186
304
  """One-pass fence scanner. Returns ``(last_stable_split, open_fence_start)``.
187
305
 
@@ -875,16 +993,36 @@ class ChatPane(VerticalScroll):
875
993
  # us enqueuing a ``scroll_end`` after every delta / tool event.
876
994
  # (a) kills the ``call_after_refresh`` backlog that piled up when
877
995
  # the UI thread was busy rendering markdown; (b) releases the anchor
878
- # when the user manually scrolls up, so they can read history
879
- # mid-stream without the viewport snapping back every 50 ms; and
880
- # (c) re-engages automatically when they return to the bottom via
881
- # ``_check_anchor``. Matches Textual's own "streaming Markdown"
996
+ # when the user manually scrolls — wheel, keyboard, or drag all go
997
+ # through ``_scroll_to`` which releases by default (widget.py:2730);
998
+ # and (c) re-engages automatically when they return to the bottom
999
+ # via ``_check_anchor``. Matches Textual's own "streaming Markdown"
882
1000
  # recipe (see ``Markdown.get_stream`` docstring).
883
1001
  self.anchor()
884
1002
  # Periodic flush; cheap because the reactive watcher already
885
1003
  # debounces repaints when buffer doesn't actually change.
886
1004
  self.set_interval(self.DEBOUNCE_SEC, self._flush_pending_delta)
887
1005
 
1006
+ def on_mouse_scroll_up(self, event) -> None:
1007
+ """Defensive redundancy — explicitly release the anchor on wheel-up.
1008
+
1009
+ Originally added under a misreading of Textual's source (see the
1010
+ Layer 8 correction in the module post-mortem). The framework's
1011
+ ``_scroll_up_for_pointer`` calls ``_scroll_to`` *without*
1012
+ ``release_anchor``, which defaults to ``True`` in
1013
+ ``widget.py:2730`` — so Textual already releases the anchor on
1014
+ wheel-up. This handler does the same thing one beat earlier and
1015
+ is effectively a no-op on the normal path.
1016
+
1017
+ Kept because (a) removing it has the same shape of change as
1018
+ keeping it and (b) if some future Textual refactor ever flips
1019
+ the default, this keeps wheel-up behaving the way ChatPane
1020
+ needs. No ``event.stop()`` — the framework handler still runs
1021
+ after this and does the actual scroll.
1022
+ """
1023
+ if self._anchored and not self._anchor_released:
1024
+ self.release_anchor()
1025
+
888
1026
  # ── API used by TextualBusSink and the App ────────────────────────
889
1027
 
890
1028
  def add_user_message(self, text: str) -> None:
@@ -924,7 +1062,11 @@ class ChatPane(VerticalScroll):
924
1062
  """
925
1063
  from textual.widgets import Static
926
1064
  self._close_active_assistant()
927
- widget = Static(renderable)
1065
+ # Sanitise the renderable's segment stream — see
1066
+ # ``_SanitizedRenderable`` docstring. This is the only content path
1067
+ # into the ChatPane that doesn't go through ``ChatMessageWidget``,
1068
+ # so it needs its own Layer-7 barrier.
1069
+ widget = Static(_SanitizedRenderable(renderable))
928
1070
  if scrollable:
929
1071
  from textual.containers import VerticalScroll
930
1072
  wrapper = VerticalScroll()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.41.0
3
+ Version: 0.42.0
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.41.0"
7
+ version = "0.42.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.41.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes