aru-code 0.44.0__tar.gz → 0.46.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 (187) hide show
  1. {aru_code-0.44.0/aru_code.egg-info → aru_code-0.46.0}/PKG-INFO +1 -1
  2. aru_code-0.46.0/aru/__init__.py +1 -0
  3. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/app.py +202 -27
  4. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/chat.py +256 -0
  5. {aru_code-0.44.0 → aru_code-0.46.0/aru_code.egg-info}/PKG-INFO +1 -1
  6. {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/SOURCES.txt +2 -0
  7. {aru_code-0.44.0 → aru_code-0.46.0}/pyproject.toml +1 -1
  8. aru_code-0.46.0/tests/test_tui_layer12_recovery.py +154 -0
  9. aru_code-0.46.0/tests/test_tui_layer13_recovery.py +116 -0
  10. aru_code-0.44.0/aru/__init__.py +0 -1
  11. {aru_code-0.44.0 → aru_code-0.46.0}/LICENSE +0 -0
  12. {aru_code-0.44.0 → aru_code-0.46.0}/README.md +0 -0
  13. {aru_code-0.44.0 → aru_code-0.46.0}/aru/agent_factory.py +0 -0
  14. {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/__init__.py +0 -0
  15. {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/base.py +0 -0
  16. {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/catalog.py +0 -0
  17. {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/planner.py +0 -0
  18. {aru_code-0.44.0 → aru_code-0.46.0}/aru/cache_patch.py +0 -0
  19. {aru_code-0.44.0 → aru_code-0.46.0}/aru/checkpoints.py +0 -0
  20. {aru_code-0.44.0 → aru_code-0.46.0}/aru/cli.py +0 -0
  21. {aru_code-0.44.0 → aru_code-0.46.0}/aru/commands.py +0 -0
  22. {aru_code-0.44.0 → aru_code-0.46.0}/aru/completers.py +0 -0
  23. {aru_code-0.44.0 → aru_code-0.46.0}/aru/config.py +0 -0
  24. {aru_code-0.44.0 → aru_code-0.46.0}/aru/context.py +0 -0
  25. {aru_code-0.44.0 → aru_code-0.46.0}/aru/display.py +0 -0
  26. {aru_code-0.44.0 → aru_code-0.46.0}/aru/events.py +0 -0
  27. {aru_code-0.44.0 → aru_code-0.46.0}/aru/format/__init__.py +0 -0
  28. {aru_code-0.44.0 → aru_code-0.46.0}/aru/format/manager.py +0 -0
  29. {aru_code-0.44.0 → aru_code-0.46.0}/aru/format/runner.py +0 -0
  30. {aru_code-0.44.0 → aru_code-0.46.0}/aru/history_blocks.py +0 -0
  31. {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/__init__.py +0 -0
  32. {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/client.py +0 -0
  33. {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/manager.py +0 -0
  34. {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/protocol.py +0 -0
  35. {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/__init__.py +0 -0
  36. {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/extractor.py +0 -0
  37. {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/loader.py +0 -0
  38. {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/store.py +0 -0
  39. {aru_code-0.44.0 → aru_code-0.46.0}/aru/permissions.py +0 -0
  40. {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugin_cache.py +0 -0
  41. {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/__init__.py +0 -0
  42. {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/custom_tools.py +0 -0
  43. {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/hooks.py +0 -0
  44. {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/manager.py +0 -0
  45. {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/tool_api.py +0 -0
  46. {aru_code-0.44.0 → aru_code-0.46.0}/aru/providers.py +0 -0
  47. {aru_code-0.44.0 → aru_code-0.46.0}/aru/runner.py +0 -0
  48. {aru_code-0.44.0 → aru_code-0.46.0}/aru/runtime.py +0 -0
  49. {aru_code-0.44.0 → aru_code-0.46.0}/aru/select.py +0 -0
  50. {aru_code-0.44.0 → aru_code-0.46.0}/aru/session.py +0 -0
  51. {aru_code-0.44.0 → aru_code-0.46.0}/aru/sinks.py +0 -0
  52. {aru_code-0.44.0 → aru_code-0.46.0}/aru/streaming.py +0 -0
  53. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tool_policy.py +0 -0
  54. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/__init__.py +0 -0
  55. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/_diff.py +0 -0
  56. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/_shared.py +0 -0
  57. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/apply_patch.py +0 -0
  58. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/apply_patch_prompt.txt +0 -0
  59. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/ast_tools.py +0 -0
  60. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/codebase.py +0 -0
  61. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/delegate.py +0 -0
  62. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/delegate_prompt.txt +0 -0
  63. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/file_ops.py +0 -0
  64. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/gitignore.py +0 -0
  65. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/lsp.py +0 -0
  66. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/mcp_client.py +0 -0
  67. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/memory_tool.py +0 -0
  68. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/plan_mode.py +0 -0
  69. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/ranker.py +0 -0
  70. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/registry.py +0 -0
  71. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/search.py +0 -0
  72. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/shell.py +0 -0
  73. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/skill.py +0 -0
  74. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/tasklist.py +0 -0
  75. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/web.py +0 -0
  76. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/worktree.py +0 -0
  77. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/__init__.py +0 -0
  78. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/sanitize.py +0 -0
  79. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/__init__.py +0 -0
  80. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/choice.py +0 -0
  81. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/confirm.py +0 -0
  82. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/search.py +0 -0
  83. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/text_input.py +0 -0
  84. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/sinks.py +0 -0
  85. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/slash_bridge.py +0 -0
  86. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/ui.py +0 -0
  87. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/__init__.py +0 -0
  88. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/completer.py +0 -0
  89. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/context_pane.py +0 -0
  90. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/header.py +0 -0
  91. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/inline_choice.py +0 -0
  92. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/loaded_pane.py +0 -0
  93. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/status.py +0 -0
  94. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/thinking.py +0 -0
  95. {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/tools.py +0 -0
  96. {aru_code-0.44.0 → aru_code-0.46.0}/aru/ui.py +0 -0
  97. {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/dependency_links.txt +0 -0
  98. {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/entry_points.txt +0 -0
  99. {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/requires.txt +0 -0
  100. {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/top_level.txt +0 -0
  101. {aru_code-0.44.0 → aru_code-0.46.0}/setup.cfg +0 -0
  102. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_agents_base.py +0 -0
  103. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_agents_md_coverage.py +0 -0
  104. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_apply_patch.py +0 -0
  105. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_async_tool_permission.py +0 -0
  106. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cache_patch_metrics.py +0 -0
  107. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cache_patch_stop_reason.py +0 -0
  108. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_catalog.py +0 -0
  109. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_chat_scrollable.py +0 -0
  110. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_checkpoints.py +0 -0
  111. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli.py +0 -0
  112. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_advanced.py +0 -0
  113. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_base.py +0 -0
  114. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_completers.py +0 -0
  115. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_new.py +0 -0
  116. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_run_cli.py +0 -0
  117. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_session.py +0 -0
  118. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_shell.py +0 -0
  119. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_codebase.py +0 -0
  120. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_confabulation_regression.py +0 -0
  121. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_config.py +0 -0
  122. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_context.py +0 -0
  123. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_context_pane.py +0 -0
  124. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cwd_awareness.py +0 -0
  125. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_delegate.py +0 -0
  126. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_events_backward_compat.py +0 -0
  127. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_events_schema.py +0 -0
  128. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_fork_ctx_concurrency.py +0 -0
  129. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_format.py +0 -0
  130. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_gitignore.py +0 -0
  131. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_guardrails_scenarios.py +0 -0
  132. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_invoke_skill.py +0 -0
  133. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_invoked_skills.py +0 -0
  134. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_loaded_pane_path.py +0 -0
  135. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_lsp.py +0 -0
  136. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_lsp_rename.py +0 -0
  137. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_main.py +0 -0
  138. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_markdown_to_text.py +0 -0
  139. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_mcp_client.py +0 -0
  140. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_mcp_health.py +0 -0
  141. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_memory.py +0 -0
  142. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_memory_tool.py +0 -0
  143. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_microcompact.py +0 -0
  144. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_permissions.py +0 -0
  145. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plan_mode_refactor.py +0 -0
  146. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugin_cache.py +0 -0
  147. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugin_errors.py +0 -0
  148. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugin_hooks_v2.py +0 -0
  149. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugins.py +0 -0
  150. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_providers.py +0 -0
  151. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_ranker.py +0 -0
  152. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_reasoning.py +0 -0
  153. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_runner_interrupt.py +0 -0
  154. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_runner_recovery.py +0 -0
  155. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_runtime.py +0 -0
  156. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_select.py +0 -0
  157. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_skill_disallowed_tools.py +0 -0
  158. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_status_breakdown.py +0 -0
  159. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_status_cost.py +0 -0
  160. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_streaming_sink.py +0 -0
  161. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tasklist.py +0 -0
  162. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_thread_tool_timeout.py +0 -0
  163. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tool_policy.py +0 -0
  164. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_truncation_marker.py +0 -0
  165. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_app_boot.py +0 -0
  166. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_bindings.py +0 -0
  167. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_bus_flow.py +0 -0
  168. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_chat.py +0 -0
  169. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_chat_adversarial.py +0 -0
  170. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_completer.py +0 -0
  171. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_completer_dynamic.py +0 -0
  172. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_copy.py +0 -0
  173. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_input_behaviour.py +0 -0
  174. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_mention_expand.py +0 -0
  175. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_modals.py +0 -0
  176. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_mode_cycle.py +0 -0
  177. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_native_selection.py +0 -0
  178. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_permission_flow.py +0 -0
  179. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_plan_task_render.py +0 -0
  180. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_sidebar_toggle.py +0 -0
  181. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_slash_bridge.py +0 -0
  182. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_snapshot_smoke.py +0 -0
  183. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_thinking_and_boot.py +0 -0
  184. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_widgets_visual.py +0 -0
  185. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_ui_adapter.py +0 -0
  186. {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_worktree.py +0 -0
  187. {aru_code-0.44.0 → aru_code-0.46.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.44.0
3
+ Version: 0.46.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.46.0"
@@ -32,6 +32,7 @@ from __future__ import annotations
32
32
 
33
33
  import asyncio
34
34
  import sys
35
+ import time
35
36
  from typing import Any
36
37
 
37
38
  from textual.app import App, ComposeResult
@@ -244,6 +245,11 @@ class AruApp(App):
244
245
  Binding("ctrl+b", "toggle_sidebar", "Sidebar", show=True),
245
246
  Binding("ctrl+y", "copy_last", "Copy last", show=True),
246
247
  Binding("ctrl+shift+y", "copy_all", "Copy all", show=False),
248
+ # Layer 13 — user-invoked terminal recovery. priority=True so the
249
+ # binding fires before any focused widget can absorb the key, in
250
+ # case Textual ever reclassifies ctrl+r as printable on some
251
+ # platform. See ``action_recover_terminal`` for what it does.
252
+ Binding("ctrl+r", "recover_terminal", "Recover", show=True, priority=True),
247
253
  Binding("up", "history_prev", "Prev", show=False, priority=False),
248
254
  Binding("down", "history_next", "Next", show=False, priority=False),
249
255
  ]
@@ -256,12 +262,24 @@ class AruApp(App):
256
262
  "skills", "agents", "commands", "mcp", "yolo",
257
263
  }
258
264
 
259
- # Layer 10 — interval (seconds) between belt-and-suspenders re-emits of
260
- # the mouse-tracking enable sequences. 8s is the worst-case time-to-recover
261
- # if a leaked DEC private-mode escape disables the wheel mid-turn; the
262
- # cost is ~24 bytes per tick (four idempotent SGR sequences). See
263
- # ``on_mount`` and ``_reenable_mouse_tracking`` for context.
264
- _MOUSE_REENABLE_INTERVAL: float = 8.0
265
+ # Layer 10 / 12 — interval (seconds) between belt-and-suspenders re-emits
266
+ # of the mouse-tracking enable sequences. Was 8s pre-Layer-12; user
267
+ # report on 2026-04-25 against ``final-fantasy-9/.aru/sessions/b33dfb99``
268
+ # was that the wheel never came back even after the turn ended in YOLO,
269
+ # and 8s was visibly long enough that the user gave up before the next
270
+ # tick. 3s is short enough that a corrupted state self-heals before the
271
+ # next mouse interaction, and the cost is still ~64 bytes per tick (the
272
+ # Layer 12 off-then-on shake — see ``_reenable_mouse_tracking``).
273
+ _MOUSE_REENABLE_INTERVAL: float = 3.0
274
+
275
+ # Layer 12 — minimum interval (seconds) between keypress-triggered
276
+ # mouse-tracking re-arms. Each keystroke is an opportunity to recover
277
+ # — if the user is typing it might be precisely BECAUSE the wheel just
278
+ # stopped working — but we don't want a fast typist to turn every
279
+ # keystroke into four extra terminal writes. 500 ms is below human
280
+ # noticeable retry latency yet caps the keystroke→write amplification
281
+ # at ~2 Hz worst case.
282
+ _KEYPRESS_REARM_DEBOUNCE: float = 0.5
265
283
 
266
284
  def __init__(
267
285
  self,
@@ -288,6 +306,12 @@ class AruApp(App):
288
306
  # cleared) by on_input_submitted.
289
307
  self._pending_paste: str | None = None
290
308
  self._pending_paste_lines: int = 0
309
+ # Layer 12 — last time we re-emitted the mouse-tracking enable
310
+ # sequences via the keypress path. Used to debounce per-keystroke
311
+ # re-arming so a fast typist doesn't spam the terminal with re-
312
+ # enables. Initialised to negative infinity so the first keystroke
313
+ # always rearms.
314
+ self._last_mouse_reenable_at: float = float("-inf")
291
315
 
292
316
  # ── Composition ──────────────────────────────────────────────────
293
317
 
@@ -549,7 +573,14 @@ class AruApp(App):
549
573
  suggestion and fires ``Input.Submitted``, which produced the
550
574
  "three Enters to run /help" glitch. Tab is the only key that
551
575
  accepts the highlighted suggestion.
576
+
577
+ Layer 12 — every keystroke is also a recovery opportunity. The
578
+ Layer 10 periodic tick still runs every ``_MOUSE_REENABLE_INTERVAL``
579
+ but a typing user wants the wheel back NOW, not in three seconds.
580
+ Debounced via ``_KEYPRESS_REARM_DEBOUNCE`` so a fast typist
581
+ doesn't amplify each keystroke into four extra terminal writes.
552
582
  """
583
+ self._maybe_rearm_mouse_on_keypress()
553
584
  try:
554
585
  completer = self.query_one(SlashCompleter)
555
586
  except Exception:
@@ -1097,34 +1128,138 @@ class AruApp(App):
1097
1128
  # without waiting for the periodic Layer 10 tick.
1098
1129
  self._reenable_mouse_tracking()
1099
1130
 
1131
+ # Layer 14 — full set of DEC private modes that ``WindowsDriver
1132
+ # .start_application_mode`` enables at boot, minus alt-screen
1133
+ # (``?1049``, not idempotent — would save/restore the display
1134
+ # buffer) and kitty-keyboard (``>1u``, terminal-specific, doesn't
1135
+ # affect wheel). Layer 13 introduced this set as a Ctrl+R-only
1136
+ # heavy shake; user confirmation that Ctrl+R actually recovered
1137
+ # the wheel after Windows display sleep/wake (2026-04-25) is the
1138
+ # signal that the broader set is what works in practice — the
1139
+ # mouse-only shake from Layer 12 was insufficient. Layer 14 promotes
1140
+ # the full set into ``_reenable_mouse_tracking`` so every existing
1141
+ # caller (Layer 9 turn boundary, Layer 10 periodic tick, Layer 12
1142
+ # broken keypress) gets the proven recovery automatically.
1143
+ _FULL_MODE_DISABLE_SEQS: tuple[str, ...] = (
1144
+ "\x1b[?1000l", # mouse VT200
1145
+ "\x1b[?1003l", # any-event mouse
1146
+ "\x1b[?1015l", # VT200 highlight mouse
1147
+ "\x1b[?1006l", # SGR ext mode mouse
1148
+ "\x1b[?1004l", # focus events
1149
+ "\x1b[?2004l", # bracketed paste
1150
+ )
1151
+ _FULL_MODE_ENABLE_SEQS: tuple[str, ...] = (
1152
+ "\x1b[?1000h",
1153
+ "\x1b[?1003h",
1154
+ "\x1b[?1015h",
1155
+ "\x1b[?1006h",
1156
+ "\x1b[?1004h",
1157
+ "\x1b[?2004h",
1158
+ )
1159
+
1100
1160
  def _reenable_mouse_tracking(self) -> None:
1101
- """Re-emit Textual's mouse-tracking enable sequences (idempotent).
1102
-
1103
- Calls the active driver's ``_enable_mouse_support`` which writes
1104
- four short SGR sequences (``?1000h`` / ``?1003h`` / ``?1015h`` /
1105
- ``?1006h``). They re-arm X10 mouse reporting at the terminal level
1106
- so a leaked ``\\x1b[?1000l`` (or any other DEC private-mode escape
1107
- that disabled wheel input) is recovered from automatically.
1108
- Idempotent sending an enable when tracking is already on is a
1109
- documented no-op on every emulator.
1110
- Called from two sites:
1111
- * ``_run_turn`` finally-clause (Layer 9) — eager recovery at every
1112
- turn boundary so the first post-turn scroll always works.
1113
- * ``_self_heal_terminal_state`` periodic tick (Layer 10) — recovers
1114
- mid-turn corruption (e.g. a leaked escape in panel content)
1115
- within ``_MOUSE_REENABLE_INTERVAL`` instead of leaving the wheel
1116
- dead until the (potentially long) turn finishes.
1117
- Wrapped in ``try/except`` because the driver may be ``None`` in
1118
- headless / test mode and the private method's exact name could
1119
- shift in a future Textual; better to no-op silently than crash.
1161
+ """Re-arm terminal modes via console-mode re-assert + full-mode shake.
1162
+
1163
+ Single recovery primitive used by every layer: turn boundary
1164
+ (Layer 9), periodic tick (Layer 10), keypress trigger (Layer 12,
1165
+ broken see chat.py post-mortem), and ``Ctrl+R`` action (Layer
1166
+ 13, which adds a refresh + chat message on top). The method
1167
+ keeps its name (``_reenable_mouse_tracking``) for git-blame
1168
+ continuity even though it now re-arms more than just mouse
1169
+ what it does is documented here, and the post-mortem in
1170
+ chat.py traces the evolution from Layer 12 through Layer 14.
1171
+
1172
+ Two failure modes the recovery handles:
1173
+
1174
+ 1. **``ENABLE_VIRTUAL_TERMINAL_INPUT`` cleared on stdin (Windows).**
1175
+ ``enable_application_mode`` (textual win32.py:179) sets this
1176
+ flag at startup, but a display sleep / wake or other Windows
1177
+ console state transition can clear it. While cleared,
1178
+ ConPTY stops translating mouse / focus events into VT
1179
+ sequences and *no* stdout escape we write can recover wheel
1180
+ input. Re-asserting the flag additively (``current | flag``)
1181
+ preserves any other input flags while ensuring VT input
1182
+ translation is back on.
1183
+
1184
+ 2. **DEC private-mode state lost on the terminal side.** Layer
1185
+ 12 originally addressed this for mouse-only via an off-then-on
1186
+ shake (``?1000l → ?1000h``) to defeat ConPTY's enable-cache.
1187
+ Layer 14 widens the shake to the full set ``WindowsDriver
1188
+ .start_application_mode`` enables: mouse (4 modes) + focus
1189
+ events (``?1004``) + bracketed paste (``?2004``). 12 escapes
1190
+ total off-then-on, ~108 bytes, one flush. Excluded:
1191
+ alt-screen (not idempotent) and kitty-keyboard (terminal-
1192
+ specific, doesn't affect wheel). The user report on
1193
+ 2026-04-25 confirmed the mouse-only shake didn't recover
1194
+ the wheel after display wake but the full shake (via Ctrl+R)
1195
+ did — Layer 14 promotes that proven recovery into the auto
1196
+ path.
1197
+
1198
+ Cost per call: ~108 bytes + one ``GetConsoleMode`` +
1199
+ ``SetConsoleMode`` syscall pair on Windows. At the 3s tick
1200
+ rate that is ~36 B/s plus microseconds — negligible.
1201
+
1202
+ Wrapped in ``try/except`` everywhere because the driver may be
1203
+ ``None`` in headless / test mode and the win32 import may fail
1204
+ on non-Windows; we'd rather no-op silently than crash.
1120
1205
  """
1206
+ if sys.platform == "win32":
1207
+ try:
1208
+ from textual.drivers.win32 import (
1209
+ ENABLE_VIRTUAL_TERMINAL_INPUT,
1210
+ get_console_mode,
1211
+ set_console_mode,
1212
+ )
1213
+ current = get_console_mode(sys.__stdin__)
1214
+ set_console_mode(
1215
+ sys.__stdin__, current | ENABLE_VIRTUAL_TERMINAL_INPUT
1216
+ )
1217
+ except Exception:
1218
+ pass
1219
+
1121
1220
  try:
1122
1221
  driver = self._driver
1123
- if driver is not None:
1124
- driver._enable_mouse_support()
1222
+ if driver is None:
1223
+ return
1224
+ for seq in self._FULL_MODE_DISABLE_SEQS:
1225
+ try:
1226
+ driver.write(seq)
1227
+ except Exception:
1228
+ pass
1229
+ for seq in self._FULL_MODE_ENABLE_SEQS:
1230
+ try:
1231
+ driver.write(seq)
1232
+ except Exception:
1233
+ pass
1234
+ try:
1235
+ driver.flush()
1236
+ except Exception:
1237
+ pass
1125
1238
  except Exception:
1126
1239
  pass
1127
1240
 
1241
+ def _maybe_rearm_mouse_on_keypress(self) -> None:
1242
+ """Layer 12 — re-arm mouse tracking on each keystroke (debounced).
1243
+
1244
+ Trigger fires from ``on_key`` so any user keypress is treated as a
1245
+ recovery opportunity. A typing user is the strongest signal we
1246
+ have that the wheel just stopped working — they reached for the
1247
+ keyboard because the mouse stopped responding, or they're about
1248
+ to scroll back with PgUp and want it ready. Either way, paying
1249
+ ~64 bytes per keypress (capped at 2 Hz by ``_KEYPRESS_REARM_DEBOUNCE``)
1250
+ is a trivial cost for sub-second recovery latency.
1251
+
1252
+ The debounce intentionally uses ``time.monotonic`` rather than the
1253
+ Textual scheduler so it survives across the async ``on_key``
1254
+ boundary without an extra task. ``-inf`` initial value guarantees
1255
+ the first keystroke always rearms.
1256
+ """
1257
+ now = time.monotonic()
1258
+ if now - self._last_mouse_reenable_at < self._KEYPRESS_REARM_DEBOUNCE:
1259
+ return
1260
+ self._last_mouse_reenable_at = now
1261
+ self._reenable_mouse_tracking()
1262
+
1128
1263
  def _self_heal_terminal_state(self) -> None:
1129
1264
  """Periodic recovery of mouse tracking and input focus (Layers 10 + 11).
1130
1265
 
@@ -1345,6 +1480,46 @@ class AruApp(App):
1345
1480
  except Exception:
1346
1481
  pass
1347
1482
 
1483
+ def action_recover_terminal(self) -> None:
1484
+ """Layer 13 — user-invoked terminal-state recovery (Ctrl+R).
1485
+
1486
+ Delegates the recovery sequence (Windows console-mode re-assert
1487
+ + full DEC private-mode shake + flush) to
1488
+ ``_reenable_mouse_tracking`` — that method now does the strong
1489
+ shake for every layer (Layer 14 promotion), so Ctrl+R, the 3s
1490
+ tick, and the turn-boundary call all run identical recovery
1491
+ bytes. This action adds two extras unique to the manual path:
1492
+
1493
+ * ``self.refresh()`` to force a compositor redraw — the
1494
+ autonomous paths don't need this because the next paint
1495
+ cycle handles it; Ctrl+R is interactive and the user wants
1496
+ immediate visible confirmation.
1497
+ * **Visible chat message** so the user sees the recovery did
1498
+ execute. The user explicitly noted that silent recovery is
1499
+ indistinguishable from no recovery, so we surface it on the
1500
+ manual path. Periodic / turn-boundary callers stay silent
1501
+ to avoid spamming the chat.
1502
+
1503
+ Bound to ``Ctrl+R`` with ``priority=True`` so the binding fires
1504
+ regardless of focused widget. Bindings dispatch via Textual's
1505
+ binding system, not through ``_on_key``, so this path is immune
1506
+ to the ``Input._on_key → event.stop()`` problem that breaks
1507
+ Layer 12's keypress trigger.
1508
+ """
1509
+ self._reenable_mouse_tracking()
1510
+
1511
+ try:
1512
+ self.refresh()
1513
+ except Exception:
1514
+ pass
1515
+
1516
+ try:
1517
+ self.query_one(ChatPane).add_system_message(
1518
+ "[Ctrl+R] Terminal modes re-armed (mouse / focus / paste)"
1519
+ )
1520
+ except Exception:
1521
+ pass
1522
+
1348
1523
  def action_toggle_sidebar(self) -> None:
1349
1524
  """Hide / show the right sidebar to give the chat full width."""
1350
1525
  try:
@@ -345,6 +345,262 @@ virtualisation experiments) would close some of these by structure,
345
345
  but at the cost of every other property the chat currently has
346
346
  (selection, copy, mid-stream insertion of arbitrary Rich panels, plan
347
347
  mounts). Layered defences are cheap and additive; the rewrite is not.
348
+
349
+ ----
350
+
351
+ Post-mortem — "self-heal didn't recover the wheel" (2026-04-25,
352
+ ``fix/scroll-analysis2``)
353
+ ---------------------------------------------------------------------
354
+ **Symptom:** wheel-dead reproduced again post-Layer-11, this time
355
+ against ``final-fantasy-9/.aru/sessions/b33dfb99``. After a YOLO turn
356
+ finished, mouse wheel stopped working on every scrollable surface
357
+ simultaneously — the canonical Layer 7/9/10 fingerprint. User report:
358
+ *"não vi nenhum self healing seu funcionar até agora"* — Layers 9
359
+ (turn-boundary call) and 10 (8 s periodic tick) ran, the four
360
+ ``?1000h``/``?1003h``/``?1015h``/``?1006h`` enables were emitted, and
361
+ yet the wheel never came back.
362
+
363
+ A byte scan of the persisted session turned up zero ``\\x1b`` bytes —
364
+ same shape as the Layer 9 incident. The leak is from a non-persisted
365
+ path (transient tool output, panel content, ConPTY-side state drift)
366
+ and we accept we won't trace its emitter. The real question Layer 12
367
+ answers is: **why didn't the recovery sequences work even when they
368
+ fired?**
369
+
370
+ **Two root causes the previous re-enable couldn't address:**
371
+
372
+ 1. **ConPTY enable-cache.** Windows ConPTY tracks DEC private-mode
373
+ state on its side. When its cache says ``?1000`` is already ``h``
374
+ it can suppress the write to the underlying terminal — even when
375
+ the terminal itself lost the state. Sending ``?1000h`` while the
376
+ cache thinks it's already on is therefore a no-op against a real
377
+ leak. Our Layer-9/10 emit was exactly this no-op.
378
+
379
+ 2. **Driver-side enable gate.** ``WindowsDriver._enable_mouse_support``
380
+ in textual 8.2.4 (``windows_driver.py:55``) opens with
381
+ ``if not self._mouse: return``. ``_mouse`` is True by default and
382
+ shouldn't flip in normal use, but our recovery path was a single
383
+ ``driver._enable_mouse_support()`` call — if any future Textual
384
+ refactor decides to flip the gate during pause / alt-screen toggle
385
+ / shutdown, every layer 9 + 10 call silently no-ops and we'd never
386
+ see the failure.
387
+
388
+ **Layer 12 — three coordinated changes** (in ``aru/tui/app.py``):
389
+
390
+ a. **Off-then-on shake instead of enable-only.**
391
+ ``_reenable_mouse_tracking`` now emits the four ``?...l`` (off)
392
+ sequences first, then the four ``?...h`` (on) sequences, then
393
+ flushes. The forced state transition defeats ConPTY's enable-cache —
394
+ the cache sees ``l→h`` and propagates the write regardless of what
395
+ it thinks the prior state was. Eight short writes (~64 bytes)
396
+ bufferised into one terminal emit by Textual's ``WriterThread``.
397
+ Idempotent: a healthy terminal lands in the same final state as
398
+ the old enable-only path, with a microscopic transition gap that
399
+ doesn't drop wheel events in practice.
400
+
401
+ b. **Bypass the driver's enable gate.** Recovery now calls
402
+ ``driver.write(...)`` for each sequence directly instead of
403
+ ``driver._enable_mouse_support()``. The four ``...l`` and four
404
+ ``...h`` strings are kept as class-level tuples
405
+ (``_MOUSE_DISABLE_SEQS`` / ``_MOUSE_ENABLE_SEQS``) so any future
406
+ call site (Click handler, focus event) reuses the exact same set
407
+ without drift, and the path is robust against Textual API changes.
408
+
409
+ c. **Keypress trigger + tighter periodic tick.**
410
+ ``_MOUSE_REENABLE_INTERVAL`` drops from 8 s to 3 s so worst-case
411
+ self-heal latency is sub-poll-cycle. ``on_key`` calls
412
+ ``_maybe_rearm_mouse_on_keypress`` which fires
413
+ ``_reenable_mouse_tracking`` on every keystroke debounced to 2 Hz
414
+ (``_KEYPRESS_REARM_DEBOUNCE = 0.5 s``). A typing user is the
415
+ strongest signal we have that they noticed the wheel just stopped
416
+ responding — recovery now happens on the very next keystroke, not
417
+ on the next tick.
418
+
419
+ Layer 12 differs from Layer 10 in the failure-mode it covers, not in
420
+ its shape. Layer 10 said "the bug will keep finding new emitters; let's
421
+ recover on a clock". Layer 12 says "even when we recover on the clock,
422
+ the recovery sequence itself can be ineffective; let's make the
423
+ recovery actually do something". The two stack: tick still runs,
424
+ keypress trigger gives it sub-second latency, off-on shake makes the
425
+ sequence the tick emits actually take effect.
426
+
427
+ ----
428
+
429
+ Post-mortem — "no self-heal of Layers 9/10/12 actually recovers"
430
+ (2026-04-25, ``fix/scroll-analysis3``)
431
+ ---------------------------------------------------------------------
432
+ **Symptom:** user reported on a Windows display-sleep/wake scenario
433
+ that the wheel dies and *no* existing self-heal recovers it. They
434
+ explicitly observed: "não vi nenhum self healing seu funcionar até
435
+ agora", and confirmed that even sending a message (which fires the
436
+ Layer 9 turn-boundary call) does not bring the wheel back.
437
+
438
+ **Two distinct root causes (one structural, one effectiveness):**
439
+
440
+ 1. **Layer 12's keypress trigger is dead during normal typing.**
441
+ ``Input._on_key`` in textual 8.x calls ``event.stop()`` for any
442
+ ``event.is_printable`` key (``textual/widgets/_input.py:736-737``).
443
+ The event is consumed at the widget level and never bubbles to
444
+ ``App.on_key``, so ``_maybe_rearm_mouse_on_keypress`` is never
445
+ invoked while the user types. The Layer 12 tests
446
+ (``tests/test_tui_layer12_recovery.py:109-116``) call the method
447
+ directly and never exercise the real path, so the bug shipped
448
+ silently. Only non-printable keys (Ctrl+combinations, arrows, Esc)
449
+ reach ``App.on_key``, and those also bypass it via the BINDINGS
450
+ system — which dispatches actions directly, again skipping
451
+ ``on_key``. In short: **Layer 12 keypress recovery never fires in
452
+ any production scenario**.
453
+
454
+ 2. **The off-then-on shake of mouse-only is not sufficient for the
455
+ user's failure mode.** Layer 9 (turn boundary) and Layer 10 (3s
456
+ tick) both fire ``_reenable_mouse_tracking``, which re-emits four
457
+ mouse DEC private modes. But ``WindowsDriver.start_application_mode``
458
+ enables a wider set at boot: mouse + focus-events (``?1004h``) +
459
+ bracketed paste (``?2004h``) + kitty-keyboard. If the display
460
+ sleep/wake corrupts more than just mouse — e.g. the entire VT
461
+ private-mode block on Windows Terminal's side — re-emitting only
462
+ mouse leaves the rest dead and the wheel doesn't come back. There
463
+ is also a possibility that ``ENABLE_VIRTUAL_TERMINAL_INPUT`` on
464
+ stdin gets cleared on wake, in which case **no** stdout escape we
465
+ write can recover wheel input regardless of what we send, because
466
+ ConPTY stops translating mouse events into VT on the input side.
467
+
468
+ **Layer 13 — user-invoked guaranteed-fire recovery (Ctrl+R).**
469
+ ``AruApp.action_recover_terminal`` (``app.py``) does four things in
470
+ order:
471
+
472
+ a. **Re-asserts ``ENABLE_VIRTUAL_TERMINAL_INPUT`` on stdin (Windows
473
+ only)** via ``set_console_mode(stdin, current | flag)``. Aditive so
474
+ other input flags survive. Closes the input-side hypothesis from
475
+ root cause 2.
476
+
477
+ b. **Off-then-on shakes the FULL DEC private mode set** —
478
+ ``_FULL_MODE_DISABLE_SEQS`` / ``_FULL_MODE_ENABLE_SEQS`` cover
479
+ mouse + focus-events + bracketed-paste. Excludes alt-screen
480
+ (``?1049``, not idempotent — would save/restore the display) and
481
+ kitty-keyboard (terminal-specific, doesn't affect wheel). 12
482
+ escapes total, ~108 bytes, one flush. Broader than Layer 12's
483
+ four-mode shake, so it catches whatever the wake corrupted even
484
+ if we don't know exactly what dropped.
485
+
486
+ c. **``self.refresh()``** to force a compositor redraw. Covers the
487
+ case where Textual's view of the screen drifted from terminal
488
+ reality.
489
+
490
+ d. **Visible system message** in the chat
491
+ (``[Ctrl+R] Terminal modes re-armed...``). The user explicitly
492
+ said silent recovery is indistinguishable from no recovery, so
493
+ we surface that the action did fire — separating "Aru's recovery
494
+ ran but the terminal still won't honor it" from "Aru never
495
+ actually tried".
496
+
497
+ Bound to ``Ctrl+R`` with ``priority=True``. Bindings dispatch through
498
+ Textual's binding system *before* the focused widget's ``_on_key``,
499
+ so this path is immune to the ``Input._on_key → event.stop()`` issue
500
+ that broke Layer 12. The user has a guaranteed-fire keystroke
501
+ regardless of focus state.
502
+
503
+ **What Layer 13 does NOT fix:**
504
+
505
+ * It does not trace the emitter — same as Layer 7/9/10/12, the cause
506
+ of the disable is still outside our reach (likely a ConPTY / Windows
507
+ Terminal interaction during display power transitions).
508
+ * It is not automatic — requires a user keystroke. Layers 9, 10, and
509
+ the (broken) 12 attempted automatic recovery; Layer 13 is the
510
+ manual fallback while we figure out which automatic trigger to
511
+ add. Candidates for a future Layer 14 if the manual shake proves
512
+ effective: hook ``AppFocus`` events, detect long gaps in the
513
+ periodic tick (wake-from-sleep signature), or move the keystroke
514
+ rearm from ``on_key`` to ``on_input_changed`` so it actually fires
515
+ during typing.
516
+
517
+ **Why this is Layer 13 and not a rewrite of Layers 9/10/12:** the
518
+ two failure modes (printable-key absorption and mouse-only-shake
519
+ insufficient) point at distinct fixes; folding them into the existing
520
+ periodic / turn-boundary callers would make every tick emit 12
521
+ escapes plus a Windows console mode call (currently 8 + nothing),
522
+ which is wasteful when the simple shake is enough — and would still
523
+ not fix the printable-key absorption bug. Keeping the strong shake
524
+ in a user-invoked path means:
525
+
526
+ * The cheap path (Layer 9/10) keeps running every 3s with mouse-only.
527
+ * The full path (Layer 13) runs only when the user signals "I
528
+ noticed the wheel is dead, please fix it".
529
+ * If the cheap path is actually sufficient most of the time, we
530
+ don't pay the heavier cost on every tick.
531
+
532
+ If, after Layer 13 ships, the user reports that ``Ctrl+R`` reliably
533
+ recovers but the automatic paths still don't, that proves the strong
534
+ shake is what works and Layers 9/10/12 should be upgraded — that's
535
+ Layer 14's signal. If even ``Ctrl+R`` doesn't recover, the hypothesis
536
+ shifts to "VT escapes are not enough, need ``SetConsoleMode`` direct
537
+ or alt-screen toggle", which is Layer 14 in the other direction.
538
+
539
+ ----
540
+
541
+ Post-mortem — "Ctrl+R recovered the wheel; promote the shake"
542
+ (2026-04-25, ``fix/scroll-analysis3`` continued)
543
+ ---------------------------------------------------------------------
544
+ **Signal:** the user reported, on the same day Layer 13 shipped, that
545
+ the wheel went dead during a session and pressing ``Ctrl+R`` brought
546
+ it back. That is the exact branch the Layer 13 post-mortem set up:
547
+ "if Ctrl+R reliably recovers but the automatic paths still don't,
548
+ that proves the strong shake is what works and Layers 9/10/12 should
549
+ be upgraded".
550
+
551
+ **Layer 14 — promote the strong shake into ``_reenable_mouse_tracking``.**
552
+ Single change: the body of ``AruApp._reenable_mouse_tracking`` now does
553
+ what Layer 13's ``action_recover_terminal`` did inline — re-assert
554
+ ``ENABLE_VIRTUAL_TERMINAL_INPUT`` on stdin (Windows) plus off-then-on
555
+ shake of the full DEC private-mode set (mouse + focus-events +
556
+ bracketed-paste). Every existing caller benefits without further
557
+ changes:
558
+
559
+ * **Layer 9** (``_run_turn`` finally clause) — every turn boundary
560
+ now runs the proven recovery, so a wheel that died mid-turn is back
561
+ before the user reaches for it.
562
+ * **Layer 10** (3s periodic tick via ``_self_heal_terminal_state``)
563
+ — autonomous recovery within 3 seconds of the wheel dying, with
564
+ the shake that actually works.
565
+ * **Layer 12** (per-keystroke trigger via ``_maybe_rearm_mouse_on_keypress``)
566
+ — still broken in production because ``Input._on_key`` consumes
567
+ printable keys before ``App.on_key`` sees them, so the trigger
568
+ never actually fires. Layer 14 doesn't fix this; the rearm primitive
569
+ is still pinned by tests for the day a future Layer wires it to
570
+ ``on_input_changed`` (which uses Message bubbling, not key events).
571
+
572
+ ``action_recover_terminal`` (Ctrl+R) now delegates the shake to
573
+ ``_reenable_mouse_tracking`` and adds two extras unique to the manual
574
+ path: ``self.refresh()`` for immediate visible repaint and the
575
+ ``[Ctrl+R] Terminal modes re-armed`` chat message. Periodic / turn-
576
+ boundary callers stay silent — running the strong shake without
577
+ spamming the chat or forcing a redraw every 3s.
578
+
579
+ **Cost of the upgrade:** every periodic tick emits 12 escapes (~108
580
+ bytes) + one ``GetConsoleMode``/``SetConsoleMode`` syscall pair on
581
+ Windows instead of 8 escapes (~64 bytes) + nothing. At 3s cadence
582
+ that is ~36 B/s plus microseconds of syscall — negligible.
583
+
584
+ **Risk acknowledged but not seen:** the off→on transition on
585
+ ``?1004`` (focus events) could in theory cause Windows Terminal to
586
+ emit a spurious focus event during the gap. Idempotent in practice
587
+ on healthy terminals (``?1004l → ?1004h`` should leave focus state
588
+ unchanged), but if a future signal points at "focus events firing
589
+ unexpectedly every 3s", the fix is to keep ``?1004`` and ``?2004`` in
590
+ the Ctrl+R-only path and revert the periodic shake to mouse-only.
591
+
592
+ **What Layer 14 does NOT change:**
593
+
594
+ * The Layer 12 keystroke trigger remains broken — ``Input._on_key``
595
+ still absorbs printable keys, so typing in the prompt never
596
+ triggers recovery. Fix candidate for a future layer: hook
597
+ ``on_input_changed`` instead of ``on_key`` (uses Message bubbling
598
+ which isn't blocked by widget consumption).
599
+ * The underlying emitter that disables terminal mouse tracking is
600
+ still unidentified — same as every layer back to 7.
601
+ * No new automatic trigger beyond the existing 3s tick + turn
602
+ boundary. Hooking ``AppFocus`` for wake-from-display-blank
603
+ detection is a follow-up if Layer 14 still misses cases.
348
604
  """
349
605
 
350
606
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.44.0
3
+ Version: 0.46.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
@@ -166,6 +166,8 @@ tests/test_tui_completer.py
166
166
  tests/test_tui_completer_dynamic.py
167
167
  tests/test_tui_copy.py
168
168
  tests/test_tui_input_behaviour.py
169
+ tests/test_tui_layer12_recovery.py
170
+ tests/test_tui_layer13_recovery.py
169
171
  tests/test_tui_mention_expand.py
170
172
  tests/test_tui_modals.py
171
173
  tests/test_tui_mode_cycle.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.44.0"
7
+ version = "0.46.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"