aru-code 0.44.0__tar.gz → 0.45.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 (186) hide show
  1. {aru_code-0.44.0/aru_code.egg-info → aru_code-0.45.0}/PKG-INFO +1 -1
  2. aru_code-0.45.0/aru/__init__.py +1 -0
  3. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/app.py +127 -23
  4. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/chat.py +78 -0
  5. {aru_code-0.44.0 → aru_code-0.45.0/aru_code.egg-info}/PKG-INFO +1 -1
  6. {aru_code-0.44.0 → aru_code-0.45.0}/aru_code.egg-info/SOURCES.txt +1 -0
  7. {aru_code-0.44.0 → aru_code-0.45.0}/pyproject.toml +1 -1
  8. aru_code-0.45.0/tests/test_tui_layer12_recovery.py +120 -0
  9. aru_code-0.44.0/aru/__init__.py +0 -1
  10. {aru_code-0.44.0 → aru_code-0.45.0}/LICENSE +0 -0
  11. {aru_code-0.44.0 → aru_code-0.45.0}/README.md +0 -0
  12. {aru_code-0.44.0 → aru_code-0.45.0}/aru/agent_factory.py +0 -0
  13. {aru_code-0.44.0 → aru_code-0.45.0}/aru/agents/__init__.py +0 -0
  14. {aru_code-0.44.0 → aru_code-0.45.0}/aru/agents/base.py +0 -0
  15. {aru_code-0.44.0 → aru_code-0.45.0}/aru/agents/catalog.py +0 -0
  16. {aru_code-0.44.0 → aru_code-0.45.0}/aru/agents/planner.py +0 -0
  17. {aru_code-0.44.0 → aru_code-0.45.0}/aru/cache_patch.py +0 -0
  18. {aru_code-0.44.0 → aru_code-0.45.0}/aru/checkpoints.py +0 -0
  19. {aru_code-0.44.0 → aru_code-0.45.0}/aru/cli.py +0 -0
  20. {aru_code-0.44.0 → aru_code-0.45.0}/aru/commands.py +0 -0
  21. {aru_code-0.44.0 → aru_code-0.45.0}/aru/completers.py +0 -0
  22. {aru_code-0.44.0 → aru_code-0.45.0}/aru/config.py +0 -0
  23. {aru_code-0.44.0 → aru_code-0.45.0}/aru/context.py +0 -0
  24. {aru_code-0.44.0 → aru_code-0.45.0}/aru/display.py +0 -0
  25. {aru_code-0.44.0 → aru_code-0.45.0}/aru/events.py +0 -0
  26. {aru_code-0.44.0 → aru_code-0.45.0}/aru/format/__init__.py +0 -0
  27. {aru_code-0.44.0 → aru_code-0.45.0}/aru/format/manager.py +0 -0
  28. {aru_code-0.44.0 → aru_code-0.45.0}/aru/format/runner.py +0 -0
  29. {aru_code-0.44.0 → aru_code-0.45.0}/aru/history_blocks.py +0 -0
  30. {aru_code-0.44.0 → aru_code-0.45.0}/aru/lsp/__init__.py +0 -0
  31. {aru_code-0.44.0 → aru_code-0.45.0}/aru/lsp/client.py +0 -0
  32. {aru_code-0.44.0 → aru_code-0.45.0}/aru/lsp/manager.py +0 -0
  33. {aru_code-0.44.0 → aru_code-0.45.0}/aru/lsp/protocol.py +0 -0
  34. {aru_code-0.44.0 → aru_code-0.45.0}/aru/memory/__init__.py +0 -0
  35. {aru_code-0.44.0 → aru_code-0.45.0}/aru/memory/extractor.py +0 -0
  36. {aru_code-0.44.0 → aru_code-0.45.0}/aru/memory/loader.py +0 -0
  37. {aru_code-0.44.0 → aru_code-0.45.0}/aru/memory/store.py +0 -0
  38. {aru_code-0.44.0 → aru_code-0.45.0}/aru/permissions.py +0 -0
  39. {aru_code-0.44.0 → aru_code-0.45.0}/aru/plugin_cache.py +0 -0
  40. {aru_code-0.44.0 → aru_code-0.45.0}/aru/plugins/__init__.py +0 -0
  41. {aru_code-0.44.0 → aru_code-0.45.0}/aru/plugins/custom_tools.py +0 -0
  42. {aru_code-0.44.0 → aru_code-0.45.0}/aru/plugins/hooks.py +0 -0
  43. {aru_code-0.44.0 → aru_code-0.45.0}/aru/plugins/manager.py +0 -0
  44. {aru_code-0.44.0 → aru_code-0.45.0}/aru/plugins/tool_api.py +0 -0
  45. {aru_code-0.44.0 → aru_code-0.45.0}/aru/providers.py +0 -0
  46. {aru_code-0.44.0 → aru_code-0.45.0}/aru/runner.py +0 -0
  47. {aru_code-0.44.0 → aru_code-0.45.0}/aru/runtime.py +0 -0
  48. {aru_code-0.44.0 → aru_code-0.45.0}/aru/select.py +0 -0
  49. {aru_code-0.44.0 → aru_code-0.45.0}/aru/session.py +0 -0
  50. {aru_code-0.44.0 → aru_code-0.45.0}/aru/sinks.py +0 -0
  51. {aru_code-0.44.0 → aru_code-0.45.0}/aru/streaming.py +0 -0
  52. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tool_policy.py +0 -0
  53. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/__init__.py +0 -0
  54. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/_diff.py +0 -0
  55. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/_shared.py +0 -0
  56. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/apply_patch.py +0 -0
  57. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/apply_patch_prompt.txt +0 -0
  58. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/ast_tools.py +0 -0
  59. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/codebase.py +0 -0
  60. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/delegate.py +0 -0
  61. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/delegate_prompt.txt +0 -0
  62. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/file_ops.py +0 -0
  63. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/gitignore.py +0 -0
  64. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/lsp.py +0 -0
  65. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/mcp_client.py +0 -0
  66. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/memory_tool.py +0 -0
  67. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/plan_mode.py +0 -0
  68. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/ranker.py +0 -0
  69. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/registry.py +0 -0
  70. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/search.py +0 -0
  71. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/shell.py +0 -0
  72. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/skill.py +0 -0
  73. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/tasklist.py +0 -0
  74. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/web.py +0 -0
  75. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tools/worktree.py +0 -0
  76. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/__init__.py +0 -0
  77. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/sanitize.py +0 -0
  78. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/screens/__init__.py +0 -0
  79. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/screens/choice.py +0 -0
  80. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/screens/confirm.py +0 -0
  81. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/screens/search.py +0 -0
  82. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/screens/text_input.py +0 -0
  83. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/sinks.py +0 -0
  84. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/slash_bridge.py +0 -0
  85. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/ui.py +0 -0
  86. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/__init__.py +0 -0
  87. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/completer.py +0 -0
  88. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/context_pane.py +0 -0
  89. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/header.py +0 -0
  90. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/inline_choice.py +0 -0
  91. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/loaded_pane.py +0 -0
  92. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/status.py +0 -0
  93. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/thinking.py +0 -0
  94. {aru_code-0.44.0 → aru_code-0.45.0}/aru/tui/widgets/tools.py +0 -0
  95. {aru_code-0.44.0 → aru_code-0.45.0}/aru/ui.py +0 -0
  96. {aru_code-0.44.0 → aru_code-0.45.0}/aru_code.egg-info/dependency_links.txt +0 -0
  97. {aru_code-0.44.0 → aru_code-0.45.0}/aru_code.egg-info/entry_points.txt +0 -0
  98. {aru_code-0.44.0 → aru_code-0.45.0}/aru_code.egg-info/requires.txt +0 -0
  99. {aru_code-0.44.0 → aru_code-0.45.0}/aru_code.egg-info/top_level.txt +0 -0
  100. {aru_code-0.44.0 → aru_code-0.45.0}/setup.cfg +0 -0
  101. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_agents_base.py +0 -0
  102. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_agents_md_coverage.py +0 -0
  103. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_apply_patch.py +0 -0
  104. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_async_tool_permission.py +0 -0
  105. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cache_patch_metrics.py +0 -0
  106. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cache_patch_stop_reason.py +0 -0
  107. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_catalog.py +0 -0
  108. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_chat_scrollable.py +0 -0
  109. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_checkpoints.py +0 -0
  110. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli.py +0 -0
  111. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_advanced.py +0 -0
  112. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_base.py +0 -0
  113. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_completers.py +0 -0
  114. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_new.py +0 -0
  115. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_run_cli.py +0 -0
  116. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_session.py +0 -0
  117. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cli_shell.py +0 -0
  118. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_codebase.py +0 -0
  119. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_confabulation_regression.py +0 -0
  120. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_config.py +0 -0
  121. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_context.py +0 -0
  122. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_context_pane.py +0 -0
  123. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_cwd_awareness.py +0 -0
  124. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_delegate.py +0 -0
  125. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_events_backward_compat.py +0 -0
  126. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_events_schema.py +0 -0
  127. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_fork_ctx_concurrency.py +0 -0
  128. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_format.py +0 -0
  129. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_gitignore.py +0 -0
  130. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_guardrails_scenarios.py +0 -0
  131. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_invoke_skill.py +0 -0
  132. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_invoked_skills.py +0 -0
  133. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_loaded_pane_path.py +0 -0
  134. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_lsp.py +0 -0
  135. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_lsp_rename.py +0 -0
  136. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_main.py +0 -0
  137. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_markdown_to_text.py +0 -0
  138. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_mcp_client.py +0 -0
  139. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_mcp_health.py +0 -0
  140. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_memory.py +0 -0
  141. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_memory_tool.py +0 -0
  142. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_microcompact.py +0 -0
  143. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_permissions.py +0 -0
  144. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_plan_mode_refactor.py +0 -0
  145. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_plugin_cache.py +0 -0
  146. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_plugin_errors.py +0 -0
  147. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_plugin_hooks_v2.py +0 -0
  148. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_plugins.py +0 -0
  149. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_providers.py +0 -0
  150. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_ranker.py +0 -0
  151. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_reasoning.py +0 -0
  152. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_runner_interrupt.py +0 -0
  153. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_runner_recovery.py +0 -0
  154. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_runtime.py +0 -0
  155. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_select.py +0 -0
  156. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_skill_disallowed_tools.py +0 -0
  157. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_status_breakdown.py +0 -0
  158. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_status_cost.py +0 -0
  159. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_streaming_sink.py +0 -0
  160. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tasklist.py +0 -0
  161. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_thread_tool_timeout.py +0 -0
  162. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tool_policy.py +0 -0
  163. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_truncation_marker.py +0 -0
  164. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_app_boot.py +0 -0
  165. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_bindings.py +0 -0
  166. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_bus_flow.py +0 -0
  167. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_chat.py +0 -0
  168. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_chat_adversarial.py +0 -0
  169. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_completer.py +0 -0
  170. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_completer_dynamic.py +0 -0
  171. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_copy.py +0 -0
  172. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_input_behaviour.py +0 -0
  173. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_mention_expand.py +0 -0
  174. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_modals.py +0 -0
  175. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_mode_cycle.py +0 -0
  176. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_native_selection.py +0 -0
  177. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_permission_flow.py +0 -0
  178. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_plan_task_render.py +0 -0
  179. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_sidebar_toggle.py +0 -0
  180. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_slash_bridge.py +0 -0
  181. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_snapshot_smoke.py +0 -0
  182. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_thinking_and_boot.py +0 -0
  183. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_tui_widgets_visual.py +0 -0
  184. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_ui_adapter.py +0 -0
  185. {aru_code-0.44.0 → aru_code-0.45.0}/tests/test_worktree.py +0 -0
  186. {aru_code-0.44.0 → aru_code-0.45.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.45.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.45.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
@@ -256,12 +257,24 @@ class AruApp(App):
256
257
  "skills", "agents", "commands", "mcp", "yolo",
257
258
  }
258
259
 
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
260
+ # Layer 10 / 12 — interval (seconds) between belt-and-suspenders re-emits
261
+ # of the mouse-tracking enable sequences. Was 8s pre-Layer-12; user
262
+ # report on 2026-04-25 against ``final-fantasy-9/.aru/sessions/b33dfb99``
263
+ # was that the wheel never came back even after the turn ended in YOLO,
264
+ # and 8s was visibly long enough that the user gave up before the next
265
+ # tick. 3s is short enough that a corrupted state self-heals before the
266
+ # next mouse interaction, and the cost is still ~64 bytes per tick (the
267
+ # Layer 12 off-then-on shake — see ``_reenable_mouse_tracking``).
268
+ _MOUSE_REENABLE_INTERVAL: float = 3.0
269
+
270
+ # Layer 12 — minimum interval (seconds) between keypress-triggered
271
+ # mouse-tracking re-arms. Each keystroke is an opportunity to recover
272
+ # — if the user is typing it might be precisely BECAUSE the wheel just
273
+ # stopped working — but we don't want a fast typist to turn every
274
+ # keystroke into four extra terminal writes. 500 ms is below human
275
+ # noticeable retry latency yet caps the keystroke→write amplification
276
+ # at ~2 Hz worst case.
277
+ _KEYPRESS_REARM_DEBOUNCE: float = 0.5
265
278
 
266
279
  def __init__(
267
280
  self,
@@ -288,6 +301,12 @@ class AruApp(App):
288
301
  # cleared) by on_input_submitted.
289
302
  self._pending_paste: str | None = None
290
303
  self._pending_paste_lines: int = 0
304
+ # Layer 12 — last time we re-emitted the mouse-tracking enable
305
+ # sequences via the keypress path. Used to debounce per-keystroke
306
+ # re-arming so a fast typist doesn't spam the terminal with re-
307
+ # enables. Initialised to negative infinity so the first keystroke
308
+ # always rearms.
309
+ self._last_mouse_reenable_at: float = float("-inf")
291
310
 
292
311
  # ── Composition ──────────────────────────────────────────────────
293
312
 
@@ -549,7 +568,14 @@ class AruApp(App):
549
568
  suggestion and fires ``Input.Submitted``, which produced the
550
569
  "three Enters to run /help" glitch. Tab is the only key that
551
570
  accepts the highlighted suggestion.
571
+
572
+ Layer 12 — every keystroke is also a recovery opportunity. The
573
+ Layer 10 periodic tick still runs every ``_MOUSE_REENABLE_INTERVAL``
574
+ but a typing user wants the wheel back NOW, not in three seconds.
575
+ Debounced via ``_KEYPRESS_REARM_DEBOUNCE`` so a fast typist
576
+ doesn't amplify each keystroke into four extra terminal writes.
552
577
  """
578
+ self._maybe_rearm_mouse_on_keypress()
553
579
  try:
554
580
  completer = self.query_one(SlashCompleter)
555
581
  except Exception:
@@ -1097,34 +1123,112 @@ class AruApp(App):
1097
1123
  # without waiting for the periodic Layer 10 tick.
1098
1124
  self._reenable_mouse_tracking()
1099
1125
 
1126
+ # Layer 12 — DEC private-mode sequences for mouse tracking. Defined
1127
+ # at class scope so both the off-then-on shake below and any future
1128
+ # caller (Click handler, focus event) can reuse the exact same set
1129
+ # without drift.
1130
+ _MOUSE_DISABLE_SEQS: tuple[str, ...] = (
1131
+ "\x1b[?1000l",
1132
+ "\x1b[?1003l",
1133
+ "\x1b[?1015l",
1134
+ "\x1b[?1006l",
1135
+ )
1136
+ _MOUSE_ENABLE_SEQS: tuple[str, ...] = (
1137
+ "\x1b[?1000h",
1138
+ "\x1b[?1003h",
1139
+ "\x1b[?1015h",
1140
+ "\x1b[?1006h",
1141
+ )
1142
+
1100
1143
  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:
1144
+ """Re-arm mouse tracking via an off-then-on shake (Layer 12).
1145
+
1146
+ Pre-Layer-12 this method delegated to the driver's
1147
+ ``_enable_mouse_support`` which writes four short SGR sequences
1148
+ (``?1000h`` / ``?1003h`` / ``?1015h`` / ``?1006h``). That worked
1149
+ when the terminal forwarded the writes verbatim, but the user
1150
+ report on 2026-04-25 against
1151
+ ``final-fantasy-9/.aru/sessions/b33dfb99`` was the wheel never
1152
+ coming back even though Layer 9 (turn-boundary call) and Layer 10
1153
+ (8s tick) both ran. Two failure modes the old code couldn't
1154
+ recover from:
1155
+
1156
+ 1. **ConPTY enable cache.** Windows ConPTY tracks DEC private-mode
1157
+ state on its side and may treat ``?1000h`` as a no-op when its
1158
+ cache says "already enabled" — even when the underlying
1159
+ terminal lost the state. Sending ``?1000l`` first forces the
1160
+ cache through a state transition so the subsequent ``?1000h``
1161
+ is propagated.
1162
+ 2. **Driver-side gate.** ``WindowsDriver._enable_mouse_support``
1163
+ opens with ``if not self._mouse: return`` (textual 8.2.4,
1164
+ windows_driver.py:55). If a future Textual flips the gate
1165
+ during shutdown / pause / alt-screen toggle, our recovery
1166
+ silently no-ops. Going through ``driver.write`` bypasses the
1167
+ gate and writes the bytes regardless.
1168
+
1169
+ Implementation: emit all four ``...l`` (off) sequences, then all
1170
+ four ``...h`` (on) sequences, then flush. Eight short writes
1171
+ (~64 bytes) bufferised into one terminal emit by ``WriterThread``.
1172
+ Idempotent on a healthy terminal — the off→on cycle leaves the
1173
+ final state identical to a single ``?1000h``, just with a
1174
+ microscopic gap during the transition (no observable wheel-event
1175
+ loss in practice).
1176
+
1177
+ Called from three sites:
1111
1178
  * ``_run_turn`` finally-clause (Layer 9) — eager recovery at every
1112
1179
  turn boundary so the first post-turn scroll always works.
1113
1180
  * ``_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.
1181
+ mid-turn corruption within ``_MOUSE_REENABLE_INTERVAL``.
1182
+ * ``on_key`` keypress trigger (Layer 12) — recovers the moment the
1183
+ user touches the keyboard, since a keystroke is a strong signal
1184
+ they noticed the wheel is dead.
1185
+
1117
1186
  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.
1187
+ headless / test mode; we'd rather no-op silently than crash.
1120
1188
  """
1121
1189
  try:
1122
1190
  driver = self._driver
1123
- if driver is not None:
1124
- driver._enable_mouse_support()
1191
+ if driver is None:
1192
+ return
1193
+ for seq in self._MOUSE_DISABLE_SEQS:
1194
+ try:
1195
+ driver.write(seq)
1196
+ except Exception:
1197
+ pass
1198
+ for seq in self._MOUSE_ENABLE_SEQS:
1199
+ try:
1200
+ driver.write(seq)
1201
+ except Exception:
1202
+ pass
1203
+ try:
1204
+ driver.flush()
1205
+ except Exception:
1206
+ pass
1125
1207
  except Exception:
1126
1208
  pass
1127
1209
 
1210
+ def _maybe_rearm_mouse_on_keypress(self) -> None:
1211
+ """Layer 12 — re-arm mouse tracking on each keystroke (debounced).
1212
+
1213
+ Trigger fires from ``on_key`` so any user keypress is treated as a
1214
+ recovery opportunity. A typing user is the strongest signal we
1215
+ have that the wheel just stopped working — they reached for the
1216
+ keyboard because the mouse stopped responding, or they're about
1217
+ to scroll back with PgUp and want it ready. Either way, paying
1218
+ ~64 bytes per keypress (capped at 2 Hz by ``_KEYPRESS_REARM_DEBOUNCE``)
1219
+ is a trivial cost for sub-second recovery latency.
1220
+
1221
+ The debounce intentionally uses ``time.monotonic`` rather than the
1222
+ Textual scheduler so it survives across the async ``on_key``
1223
+ boundary without an extra task. ``-inf`` initial value guarantees
1224
+ the first keystroke always rearms.
1225
+ """
1226
+ now = time.monotonic()
1227
+ if now - self._last_mouse_reenable_at < self._KEYPRESS_REARM_DEBOUNCE:
1228
+ return
1229
+ self._last_mouse_reenable_at = now
1230
+ self._reenable_mouse_tracking()
1231
+
1128
1232
  def _self_heal_terminal_state(self) -> None:
1129
1233
  """Periodic recovery of mouse tracking and input focus (Layers 10 + 11).
1130
1234
 
@@ -345,6 +345,84 @@ 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.
348
426
  """
349
427
 
350
428
  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.45.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,7 @@ 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
169
170
  tests/test_tui_mention_expand.py
170
171
  tests/test_tui_modals.py
171
172
  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.45.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,120 @@
1
+ """Layer 12 — mouse-tracking recovery: off-then-on shake + keypress trigger.
2
+
3
+ Background: ``aru/tui/widgets/chat.py`` post-mortem under "self-heal didn't
4
+ recover the wheel" (2026-04-25). Layers 9/10 emitted only the four ``?...h``
5
+ enable sequences; if ConPTY's enable-cache or the driver's gate suppressed
6
+ the write, no recovery happened. Layer 12 emits a forced ``?...l → ?...h``
7
+ state transition via ``driver.write`` and adds a per-keypress trigger so the
8
+ user gets sub-second recovery instead of waiting for the periodic tick.
9
+
10
+ These tests pin down the observable contracts:
11
+ * the eight DEC private-mode sequences are emitted in disable→enable order;
12
+ * the keypress trigger calls into ``_reenable_mouse_tracking`` debounced by
13
+ ``_KEYPRESS_REARM_DEBOUNCE``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import pytest
19
+
20
+ pytest.importorskip("textual")
21
+
22
+
23
+ class _RecordingDriver:
24
+ """Driver stub that records every ``write`` call."""
25
+
26
+ def __init__(self) -> None:
27
+ self.writes: list[str] = []
28
+ self.flushes: int = 0
29
+
30
+ def write(self, data: str) -> None:
31
+ self.writes.append(data)
32
+
33
+ def flush(self) -> None:
34
+ self.flushes += 1
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_reenable_mouse_tracking_emits_off_then_on_shake():
39
+ """Layer 12: ``?...l`` (4) is emitted *before* ``?...h`` (4), then flush.
40
+
41
+ The off→on shake forces ConPTY's enable-cache through a state
42
+ transition, defeating the case where its cache claims ``?1000`` is
43
+ already ``h`` and suppresses the propagated write. Order matters —
44
+ if the on sequences came first the cache could no-op them.
45
+ """
46
+ from aru.tui.app import AruApp
47
+
48
+ app = AruApp()
49
+ rec = _RecordingDriver()
50
+ # ``_driver`` is a private slot of ``App`` — assigning directly
51
+ # short-circuits the application-mode startup that would normally
52
+ # set it. The recovery method only reads ``self._driver``.
53
+ app._driver = rec
54
+
55
+ app._reenable_mouse_tracking()
56
+
57
+ expected_off = [
58
+ "\x1b[?1000l",
59
+ "\x1b[?1003l",
60
+ "\x1b[?1015l",
61
+ "\x1b[?1006l",
62
+ ]
63
+ expected_on = [
64
+ "\x1b[?1000h",
65
+ "\x1b[?1003h",
66
+ "\x1b[?1015h",
67
+ "\x1b[?1006h",
68
+ ]
69
+ assert rec.writes == expected_off + expected_on
70
+ # One flush at the end — the ``WriterThread`` bufferises everything
71
+ # before that into a single terminal emit.
72
+ assert rec.flushes == 1
73
+
74
+
75
+ @pytest.mark.asyncio
76
+ async def test_reenable_mouse_tracking_no_driver_is_noop():
77
+ """If the driver is ``None`` (headless / pre-mount) the call is a quiet no-op."""
78
+ from aru.tui.app import AruApp
79
+
80
+ app = AruApp()
81
+ app._driver = None
82
+ # Must not raise.
83
+ app._reenable_mouse_tracking()
84
+
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_keypress_rearm_is_debounced(monkeypatch):
88
+ """Layer 12 keypress trigger respects ``_KEYPRESS_REARM_DEBOUNCE``.
89
+
90
+ Two keystrokes within the debounce window should produce exactly one
91
+ ``_reenable_mouse_tracking`` invocation; a third keystroke after the
92
+ window elapses should produce a second.
93
+ """
94
+ from aru.tui import app as app_mod
95
+ from aru.tui.app import AruApp
96
+
97
+ app = AruApp()
98
+ rec = _RecordingDriver()
99
+ app._driver = rec
100
+
101
+ fake_now = [100.0]
102
+
103
+ def fake_monotonic() -> float:
104
+ return fake_now[0]
105
+
106
+ monkeypatch.setattr(app_mod.time, "monotonic", fake_monotonic)
107
+
108
+ # 1st keystroke at t=100 — fires.
109
+ app._maybe_rearm_mouse_on_keypress()
110
+ # 2nd keystroke 100 ms later — within 500 ms debounce → suppressed.
111
+ fake_now[0] += 0.1
112
+ app._maybe_rearm_mouse_on_keypress()
113
+ # 3rd keystroke 600 ms after 1st (i.e. 500 ms after debounce window
114
+ # opened) — fires.
115
+ fake_now[0] += 0.5
116
+ app._maybe_rearm_mouse_on_keypress()
117
+
118
+ # Each fired call emits 8 sequences (4 off + 4 on). Two fires = 16.
119
+ assert len(rec.writes) == 16
120
+ assert rec.flushes == 2
@@ -1 +0,0 @@
1
- __version__ = "0.44.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
File without changes