aru-code 0.45.0__tar.gz → 0.47.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 (188) hide show
  1. {aru_code-0.45.0/aru_code.egg-info → aru_code-0.47.0}/PKG-INFO +1 -1
  2. aru_code-0.47.0/aru/__init__.py +1 -0
  3. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/app.py +264 -56
  4. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/chat.py +178 -0
  5. {aru_code-0.45.0 → aru_code-0.47.0/aru_code.egg-info}/PKG-INFO +1 -1
  6. {aru_code-0.45.0 → aru_code-0.47.0}/aru_code.egg-info/SOURCES.txt +2 -0
  7. {aru_code-0.45.0 → aru_code-0.47.0}/pyproject.toml +1 -1
  8. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_layer12_recovery.py +51 -17
  9. aru_code-0.47.0/tests/test_tui_layer13_recovery.py +116 -0
  10. aru_code-0.47.0/tests/test_tui_shell_bang.py +183 -0
  11. aru_code-0.45.0/aru/__init__.py +0 -1
  12. {aru_code-0.45.0 → aru_code-0.47.0}/LICENSE +0 -0
  13. {aru_code-0.45.0 → aru_code-0.47.0}/README.md +0 -0
  14. {aru_code-0.45.0 → aru_code-0.47.0}/aru/agent_factory.py +0 -0
  15. {aru_code-0.45.0 → aru_code-0.47.0}/aru/agents/__init__.py +0 -0
  16. {aru_code-0.45.0 → aru_code-0.47.0}/aru/agents/base.py +0 -0
  17. {aru_code-0.45.0 → aru_code-0.47.0}/aru/agents/catalog.py +0 -0
  18. {aru_code-0.45.0 → aru_code-0.47.0}/aru/agents/planner.py +0 -0
  19. {aru_code-0.45.0 → aru_code-0.47.0}/aru/cache_patch.py +0 -0
  20. {aru_code-0.45.0 → aru_code-0.47.0}/aru/checkpoints.py +0 -0
  21. {aru_code-0.45.0 → aru_code-0.47.0}/aru/cli.py +0 -0
  22. {aru_code-0.45.0 → aru_code-0.47.0}/aru/commands.py +0 -0
  23. {aru_code-0.45.0 → aru_code-0.47.0}/aru/completers.py +0 -0
  24. {aru_code-0.45.0 → aru_code-0.47.0}/aru/config.py +0 -0
  25. {aru_code-0.45.0 → aru_code-0.47.0}/aru/context.py +0 -0
  26. {aru_code-0.45.0 → aru_code-0.47.0}/aru/display.py +0 -0
  27. {aru_code-0.45.0 → aru_code-0.47.0}/aru/events.py +0 -0
  28. {aru_code-0.45.0 → aru_code-0.47.0}/aru/format/__init__.py +0 -0
  29. {aru_code-0.45.0 → aru_code-0.47.0}/aru/format/manager.py +0 -0
  30. {aru_code-0.45.0 → aru_code-0.47.0}/aru/format/runner.py +0 -0
  31. {aru_code-0.45.0 → aru_code-0.47.0}/aru/history_blocks.py +0 -0
  32. {aru_code-0.45.0 → aru_code-0.47.0}/aru/lsp/__init__.py +0 -0
  33. {aru_code-0.45.0 → aru_code-0.47.0}/aru/lsp/client.py +0 -0
  34. {aru_code-0.45.0 → aru_code-0.47.0}/aru/lsp/manager.py +0 -0
  35. {aru_code-0.45.0 → aru_code-0.47.0}/aru/lsp/protocol.py +0 -0
  36. {aru_code-0.45.0 → aru_code-0.47.0}/aru/memory/__init__.py +0 -0
  37. {aru_code-0.45.0 → aru_code-0.47.0}/aru/memory/extractor.py +0 -0
  38. {aru_code-0.45.0 → aru_code-0.47.0}/aru/memory/loader.py +0 -0
  39. {aru_code-0.45.0 → aru_code-0.47.0}/aru/memory/store.py +0 -0
  40. {aru_code-0.45.0 → aru_code-0.47.0}/aru/permissions.py +0 -0
  41. {aru_code-0.45.0 → aru_code-0.47.0}/aru/plugin_cache.py +0 -0
  42. {aru_code-0.45.0 → aru_code-0.47.0}/aru/plugins/__init__.py +0 -0
  43. {aru_code-0.45.0 → aru_code-0.47.0}/aru/plugins/custom_tools.py +0 -0
  44. {aru_code-0.45.0 → aru_code-0.47.0}/aru/plugins/hooks.py +0 -0
  45. {aru_code-0.45.0 → aru_code-0.47.0}/aru/plugins/manager.py +0 -0
  46. {aru_code-0.45.0 → aru_code-0.47.0}/aru/plugins/tool_api.py +0 -0
  47. {aru_code-0.45.0 → aru_code-0.47.0}/aru/providers.py +0 -0
  48. {aru_code-0.45.0 → aru_code-0.47.0}/aru/runner.py +0 -0
  49. {aru_code-0.45.0 → aru_code-0.47.0}/aru/runtime.py +0 -0
  50. {aru_code-0.45.0 → aru_code-0.47.0}/aru/select.py +0 -0
  51. {aru_code-0.45.0 → aru_code-0.47.0}/aru/session.py +0 -0
  52. {aru_code-0.45.0 → aru_code-0.47.0}/aru/sinks.py +0 -0
  53. {aru_code-0.45.0 → aru_code-0.47.0}/aru/streaming.py +0 -0
  54. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tool_policy.py +0 -0
  55. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/__init__.py +0 -0
  56. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/_diff.py +0 -0
  57. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/_shared.py +0 -0
  58. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/apply_patch.py +0 -0
  59. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/apply_patch_prompt.txt +0 -0
  60. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/ast_tools.py +0 -0
  61. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/codebase.py +0 -0
  62. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/delegate.py +0 -0
  63. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/delegate_prompt.txt +0 -0
  64. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/file_ops.py +0 -0
  65. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/gitignore.py +0 -0
  66. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/lsp.py +0 -0
  67. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/mcp_client.py +0 -0
  68. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/memory_tool.py +0 -0
  69. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/plan_mode.py +0 -0
  70. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/ranker.py +0 -0
  71. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/registry.py +0 -0
  72. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/search.py +0 -0
  73. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/shell.py +0 -0
  74. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/skill.py +0 -0
  75. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/tasklist.py +0 -0
  76. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/web.py +0 -0
  77. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tools/worktree.py +0 -0
  78. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/__init__.py +0 -0
  79. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/sanitize.py +0 -0
  80. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/screens/__init__.py +0 -0
  81. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/screens/choice.py +0 -0
  82. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/screens/confirm.py +0 -0
  83. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/screens/search.py +0 -0
  84. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/screens/text_input.py +0 -0
  85. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/sinks.py +0 -0
  86. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/slash_bridge.py +0 -0
  87. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/ui.py +0 -0
  88. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/__init__.py +0 -0
  89. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/completer.py +0 -0
  90. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/context_pane.py +0 -0
  91. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/header.py +0 -0
  92. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/inline_choice.py +0 -0
  93. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/loaded_pane.py +0 -0
  94. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/status.py +0 -0
  95. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/thinking.py +0 -0
  96. {aru_code-0.45.0 → aru_code-0.47.0}/aru/tui/widgets/tools.py +0 -0
  97. {aru_code-0.45.0 → aru_code-0.47.0}/aru/ui.py +0 -0
  98. {aru_code-0.45.0 → aru_code-0.47.0}/aru_code.egg-info/dependency_links.txt +0 -0
  99. {aru_code-0.45.0 → aru_code-0.47.0}/aru_code.egg-info/entry_points.txt +0 -0
  100. {aru_code-0.45.0 → aru_code-0.47.0}/aru_code.egg-info/requires.txt +0 -0
  101. {aru_code-0.45.0 → aru_code-0.47.0}/aru_code.egg-info/top_level.txt +0 -0
  102. {aru_code-0.45.0 → aru_code-0.47.0}/setup.cfg +0 -0
  103. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_agents_base.py +0 -0
  104. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_agents_md_coverage.py +0 -0
  105. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_apply_patch.py +0 -0
  106. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_async_tool_permission.py +0 -0
  107. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cache_patch_metrics.py +0 -0
  108. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cache_patch_stop_reason.py +0 -0
  109. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_catalog.py +0 -0
  110. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_chat_scrollable.py +0 -0
  111. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_checkpoints.py +0 -0
  112. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli.py +0 -0
  113. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_advanced.py +0 -0
  114. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_base.py +0 -0
  115. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_completers.py +0 -0
  116. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_new.py +0 -0
  117. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_run_cli.py +0 -0
  118. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_session.py +0 -0
  119. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cli_shell.py +0 -0
  120. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_codebase.py +0 -0
  121. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_confabulation_regression.py +0 -0
  122. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_config.py +0 -0
  123. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_context.py +0 -0
  124. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_context_pane.py +0 -0
  125. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_cwd_awareness.py +0 -0
  126. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_delegate.py +0 -0
  127. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_events_backward_compat.py +0 -0
  128. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_events_schema.py +0 -0
  129. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_fork_ctx_concurrency.py +0 -0
  130. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_format.py +0 -0
  131. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_gitignore.py +0 -0
  132. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_guardrails_scenarios.py +0 -0
  133. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_invoke_skill.py +0 -0
  134. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_invoked_skills.py +0 -0
  135. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_loaded_pane_path.py +0 -0
  136. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_lsp.py +0 -0
  137. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_lsp_rename.py +0 -0
  138. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_main.py +0 -0
  139. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_markdown_to_text.py +0 -0
  140. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_mcp_client.py +0 -0
  141. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_mcp_health.py +0 -0
  142. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_memory.py +0 -0
  143. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_memory_tool.py +0 -0
  144. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_microcompact.py +0 -0
  145. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_permissions.py +0 -0
  146. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_plan_mode_refactor.py +0 -0
  147. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_plugin_cache.py +0 -0
  148. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_plugin_errors.py +0 -0
  149. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_plugin_hooks_v2.py +0 -0
  150. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_plugins.py +0 -0
  151. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_providers.py +0 -0
  152. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_ranker.py +0 -0
  153. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_reasoning.py +0 -0
  154. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_runner_interrupt.py +0 -0
  155. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_runner_recovery.py +0 -0
  156. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_runtime.py +0 -0
  157. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_select.py +0 -0
  158. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_skill_disallowed_tools.py +0 -0
  159. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_status_breakdown.py +0 -0
  160. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_status_cost.py +0 -0
  161. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_streaming_sink.py +0 -0
  162. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tasklist.py +0 -0
  163. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_thread_tool_timeout.py +0 -0
  164. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tool_policy.py +0 -0
  165. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_truncation_marker.py +0 -0
  166. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_app_boot.py +0 -0
  167. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_bindings.py +0 -0
  168. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_bus_flow.py +0 -0
  169. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_chat.py +0 -0
  170. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_chat_adversarial.py +0 -0
  171. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_completer.py +0 -0
  172. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_completer_dynamic.py +0 -0
  173. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_copy.py +0 -0
  174. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_input_behaviour.py +0 -0
  175. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_mention_expand.py +0 -0
  176. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_modals.py +0 -0
  177. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_mode_cycle.py +0 -0
  178. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_native_selection.py +0 -0
  179. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_permission_flow.py +0 -0
  180. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_plan_task_render.py +0 -0
  181. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_sidebar_toggle.py +0 -0
  182. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_slash_bridge.py +0 -0
  183. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_snapshot_smoke.py +0 -0
  184. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_thinking_and_boot.py +0 -0
  185. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_tui_widgets_visual.py +0 -0
  186. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_ui_adapter.py +0 -0
  187. {aru_code-0.45.0 → aru_code-0.47.0}/tests/test_worktree.py +0 -0
  188. {aru_code-0.45.0 → aru_code-0.47.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.45.0
3
+ Version: 0.47.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.47.0"
@@ -245,6 +245,11 @@ class AruApp(App):
245
245
  Binding("ctrl+b", "toggle_sidebar", "Sidebar", show=True),
246
246
  Binding("ctrl+y", "copy_last", "Copy last", show=True),
247
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),
248
253
  Binding("up", "history_prev", "Prev", show=False, priority=False),
249
254
  Binding("down", "history_next", "Next", show=False, priority=False),
250
255
  ]
@@ -642,6 +647,27 @@ class AruApp(App):
642
647
  if text.startswith("/") and self._maybe_run_local_slash(text):
643
648
  return
644
649
 
650
+ # Shell escape: ``! <command>`` runs the command locally in the
651
+ # session cwd and streams output into the chat. Mirrors the REPL
652
+ # path in ``cli.py`` so users can do quick ``! git status`` or
653
+ # ``! ls`` without a round-trip to the agent. The leading ``!``
654
+ # must be followed by whitespace so plain text starting with ``!``
655
+ # (rare but possible) still reaches the agent.
656
+ if text.startswith("!"):
657
+ cmd = text[1:].lstrip()
658
+ if not cmd:
659
+ self.query_one(ChatPane).add_system_message(
660
+ "Usage: ! <command>"
661
+ )
662
+ return
663
+ if self._busy:
664
+ self.query_one(ChatPane).add_system_message(
665
+ "Busy — wait for the current task to finish."
666
+ )
667
+ return
668
+ self._dispatch_shell_command(cmd)
669
+ return
670
+
645
671
  if self._busy:
646
672
  self.query_one(ChatPane).add_system_message(
647
673
  "Agent is busy — wait for the current turn to finish."
@@ -958,6 +984,7 @@ class AruApp(App):
958
984
  " /clear clear chat pane",
959
985
  " /plan toggle plan mode",
960
986
  " /quit /exit save session and exit",
987
+ " ! <command> run a shell command (output streams to chat)",
961
988
  "",
962
989
  "Shortcuts:",
963
990
  " Ctrl+Q quit",
@@ -1123,79 +1150,220 @@ class AruApp(App):
1123
1150
  # without waiting for the periodic Layer 10 tick.
1124
1151
  self._reenable_mouse_tracking()
1125
1152
 
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",
1153
+ # ── Shell escape (``! <command>``) ───────────────────────────────
1154
+
1155
+ def _dispatch_shell_command(self, command: str) -> None:
1156
+ """Run ``command`` in the session cwd and stream output to chat.
1157
+
1158
+ Parity with the REPL's ``! <cmd>`` path in ``cli.py``: we render
1159
+ a syntax-highlighted header, run the command via the system
1160
+ shell, then push stdout/stderr (interleaved) into a single
1161
+ system message that grows as lines arrive. The exit code is
1162
+ appended on completion so the user can tell success from
1163
+ failure.
1164
+
1165
+ Output is NOT persisted to ``session.history`` — the agent never
1166
+ sees ``!`` shell runs (it has its own ``bash`` tool). This is a
1167
+ user convenience, not part of the conversation.
1168
+ """
1169
+ chat = self.query_one(ChatPane)
1170
+ try:
1171
+ from rich.panel import Panel
1172
+ from rich.syntax import Syntax
1173
+ chat.add_renderable(Panel(
1174
+ Syntax(command, "bash", theme="monokai"),
1175
+ title="[bold]Shell[/bold]",
1176
+ border_style="dim",
1177
+ expand=False,
1178
+ ))
1179
+ except Exception:
1180
+ chat.add_system_message(f"$ {command}")
1181
+
1182
+ from aru.tui.widgets.chat import ChatMessageWidget
1183
+ live = ChatMessageWidget(role="system", initial="")
1184
+ chat.mount(live)
1185
+ self._busy = True
1186
+ try:
1187
+ self.query_one(ThinkingIndicator).busy = True
1188
+ except Exception:
1189
+ pass
1190
+ self.run_worker(
1191
+ self._run_shell_command(command, live),
1192
+ name="shell-cmd",
1193
+ exclusive=False,
1194
+ group="shell",
1195
+ )
1196
+
1197
+ async def _run_shell_command(
1198
+ self, command: str, live: "ChatMessageWidget"
1199
+ ) -> None:
1200
+ """Spawn ``command`` and stream output into ``live`` line by line."""
1201
+ import asyncio
1202
+
1203
+ try:
1204
+ from aru.runtime import get_cwd
1205
+ cwd = get_cwd()
1206
+ except Exception:
1207
+ import os
1208
+ cwd = os.getcwd()
1209
+
1210
+ try:
1211
+ proc = await asyncio.create_subprocess_shell(
1212
+ command,
1213
+ stdout=asyncio.subprocess.PIPE,
1214
+ stderr=asyncio.subprocess.STDOUT,
1215
+ cwd=cwd,
1216
+ )
1217
+ except Exception as exc:
1218
+ live.buffer = f"[shell error] {type(exc).__name__}: {exc}"
1219
+ self._busy = False
1220
+ try:
1221
+ self.query_one(ThinkingIndicator).busy = False
1222
+ except Exception:
1223
+ pass
1224
+ return
1225
+
1226
+ assert proc.stdout is not None
1227
+ buffer_lines: list[str] = []
1228
+ try:
1229
+ while True:
1230
+ raw = await proc.stdout.readline()
1231
+ if not raw:
1232
+ break
1233
+ line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
1234
+ buffer_lines.append(line)
1235
+ # Cap displayed buffer so a runaway command doesn't grow
1236
+ # the widget until the chat pane stalls. Mirrors the
1237
+ # ``bash`` tool's 10K-char output truncation.
1238
+ joined = "\n".join(buffer_lines)
1239
+ if len(joined) > 10_000:
1240
+ head = joined[:10_000]
1241
+ live.buffer = head + "\n... (truncated, still running)"
1242
+ else:
1243
+ live.buffer = joined
1244
+ await proc.wait()
1245
+ except asyncio.CancelledError:
1246
+ try:
1247
+ proc.kill()
1248
+ except Exception:
1249
+ pass
1250
+ live.buffer = (live.buffer or "") + "\n[interrupted]"
1251
+ raise
1252
+ except Exception as exc:
1253
+ live.buffer = (live.buffer or "") + (
1254
+ f"\n[shell error] {type(exc).__name__}: {exc}"
1255
+ )
1256
+ finally:
1257
+ rc = proc.returncode if proc.returncode is not None else "?"
1258
+ tail = f"\n[exit {rc}]"
1259
+ current = live.buffer or ""
1260
+ if not current.endswith(tail):
1261
+ live.buffer = current + tail
1262
+ self._busy = False
1263
+ try:
1264
+ self.query_one(ThinkingIndicator).busy = False
1265
+ except Exception:
1266
+ pass
1267
+
1268
+ # Layer 14 — full set of DEC private modes that ``WindowsDriver
1269
+ # .start_application_mode`` enables at boot, minus alt-screen
1270
+ # (``?1049``, not idempotent — would save/restore the display
1271
+ # buffer) and kitty-keyboard (``>1u``, terminal-specific, doesn't
1272
+ # affect wheel). Layer 13 introduced this set as a Ctrl+R-only
1273
+ # heavy shake; user confirmation that Ctrl+R actually recovered
1274
+ # the wheel after Windows display sleep/wake (2026-04-25) is the
1275
+ # signal that the broader set is what works in practice — the
1276
+ # mouse-only shake from Layer 12 was insufficient. Layer 14 promotes
1277
+ # the full set into ``_reenable_mouse_tracking`` so every existing
1278
+ # caller (Layer 9 turn boundary, Layer 10 periodic tick, Layer 12
1279
+ # broken keypress) gets the proven recovery automatically.
1280
+ _FULL_MODE_DISABLE_SEQS: tuple[str, ...] = (
1281
+ "\x1b[?1000l", # mouse VT200
1282
+ "\x1b[?1003l", # any-event mouse
1283
+ "\x1b[?1015l", # VT200 highlight mouse
1284
+ "\x1b[?1006l", # SGR ext mode mouse
1285
+ "\x1b[?1004l", # focus events
1286
+ "\x1b[?2004l", # bracketed paste
1135
1287
  )
1136
- _MOUSE_ENABLE_SEQS: tuple[str, ...] = (
1288
+ _FULL_MODE_ENABLE_SEQS: tuple[str, ...] = (
1137
1289
  "\x1b[?1000h",
1138
1290
  "\x1b[?1003h",
1139
1291
  "\x1b[?1015h",
1140
1292
  "\x1b[?1006h",
1293
+ "\x1b[?1004h",
1294
+ "\x1b[?2004h",
1141
1295
  )
1142
1296
 
1143
1297
  def _reenable_mouse_tracking(self) -> None:
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:
1178
- * ``_run_turn`` finally-clause (Layer 9) eager recovery at every
1179
- turn boundary so the first post-turn scroll always works.
1180
- * ``_self_heal_terminal_state`` periodic tick (Layer 10) — recovers
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
-
1186
- Wrapped in ``try/except`` because the driver may be ``None`` in
1187
- headless / test mode; we'd rather no-op silently than crash.
1298
+ """Re-arm terminal modes via console-mode re-assert + full-mode shake.
1299
+
1300
+ Single recovery primitive used by every layer: turn boundary
1301
+ (Layer 9), periodic tick (Layer 10), keypress trigger (Layer 12,
1302
+ broken see chat.py post-mortem), and ``Ctrl+R`` action (Layer
1303
+ 13, which adds a refresh + chat message on top). The method
1304
+ keeps its name (``_reenable_mouse_tracking``) for git-blame
1305
+ continuity even though it now re-arms more than just mouse —
1306
+ what it does is documented here, and the post-mortem in
1307
+ chat.py traces the evolution from Layer 12 through Layer 14.
1308
+
1309
+ Two failure modes the recovery handles:
1310
+
1311
+ 1. **``ENABLE_VIRTUAL_TERMINAL_INPUT`` cleared on stdin (Windows).**
1312
+ ``enable_application_mode`` (textual win32.py:179) sets this
1313
+ flag at startup, but a display sleep / wake or other Windows
1314
+ console state transition can clear it. While cleared,
1315
+ ConPTY stops translating mouse / focus events into VT
1316
+ sequences and *no* stdout escape we write can recover wheel
1317
+ input. Re-asserting the flag additively (``current | flag``)
1318
+ preserves any other input flags while ensuring VT input
1319
+ translation is back on.
1320
+
1321
+ 2. **DEC private-mode state lost on the terminal side.** Layer
1322
+ 12 originally addressed this for mouse-only via an off-then-on
1323
+ shake (``?1000l ?1000h``) to defeat ConPTY's enable-cache.
1324
+ Layer 14 widens the shake to the full set ``WindowsDriver
1325
+ .start_application_mode`` enables: mouse (4 modes) + focus
1326
+ events (``?1004``) + bracketed paste (``?2004``). 12 escapes
1327
+ total off-then-on, ~108 bytes, one flush. Excluded:
1328
+ alt-screen (not idempotent) and kitty-keyboard (terminal-
1329
+ specific, doesn't affect wheel). The user report on
1330
+ 2026-04-25 confirmed the mouse-only shake didn't recover
1331
+ the wheel after display wake but the full shake (via Ctrl+R)
1332
+ did Layer 14 promotes that proven recovery into the auto
1333
+ path.
1334
+
1335
+ Cost per call: ~108 bytes + one ``GetConsoleMode`` +
1336
+ ``SetConsoleMode`` syscall pair on Windows. At the 3s tick
1337
+ rate that is ~36 B/s plus microseconds negligible.
1338
+
1339
+ Wrapped in ``try/except`` everywhere because the driver may be
1340
+ ``None`` in headless / test mode and the win32 import may fail
1341
+ on non-Windows; we'd rather no-op silently than crash.
1188
1342
  """
1343
+ if sys.platform == "win32":
1344
+ try:
1345
+ from textual.drivers.win32 import (
1346
+ ENABLE_VIRTUAL_TERMINAL_INPUT,
1347
+ get_console_mode,
1348
+ set_console_mode,
1349
+ )
1350
+ current = get_console_mode(sys.__stdin__)
1351
+ set_console_mode(
1352
+ sys.__stdin__, current | ENABLE_VIRTUAL_TERMINAL_INPUT
1353
+ )
1354
+ except Exception:
1355
+ pass
1356
+
1189
1357
  try:
1190
1358
  driver = self._driver
1191
1359
  if driver is None:
1192
1360
  return
1193
- for seq in self._MOUSE_DISABLE_SEQS:
1361
+ for seq in self._FULL_MODE_DISABLE_SEQS:
1194
1362
  try:
1195
1363
  driver.write(seq)
1196
1364
  except Exception:
1197
1365
  pass
1198
- for seq in self._MOUSE_ENABLE_SEQS:
1366
+ for seq in self._FULL_MODE_ENABLE_SEQS:
1199
1367
  try:
1200
1368
  driver.write(seq)
1201
1369
  except Exception:
@@ -1449,6 +1617,46 @@ class AruApp(App):
1449
1617
  except Exception:
1450
1618
  pass
1451
1619
 
1620
+ def action_recover_terminal(self) -> None:
1621
+ """Layer 13 — user-invoked terminal-state recovery (Ctrl+R).
1622
+
1623
+ Delegates the recovery sequence (Windows console-mode re-assert
1624
+ + full DEC private-mode shake + flush) to
1625
+ ``_reenable_mouse_tracking`` — that method now does the strong
1626
+ shake for every layer (Layer 14 promotion), so Ctrl+R, the 3s
1627
+ tick, and the turn-boundary call all run identical recovery
1628
+ bytes. This action adds two extras unique to the manual path:
1629
+
1630
+ * ``self.refresh()`` to force a compositor redraw — the
1631
+ autonomous paths don't need this because the next paint
1632
+ cycle handles it; Ctrl+R is interactive and the user wants
1633
+ immediate visible confirmation.
1634
+ * **Visible chat message** so the user sees the recovery did
1635
+ execute. The user explicitly noted that silent recovery is
1636
+ indistinguishable from no recovery, so we surface it on the
1637
+ manual path. Periodic / turn-boundary callers stay silent
1638
+ to avoid spamming the chat.
1639
+
1640
+ Bound to ``Ctrl+R`` with ``priority=True`` so the binding fires
1641
+ regardless of focused widget. Bindings dispatch via Textual's
1642
+ binding system, not through ``_on_key``, so this path is immune
1643
+ to the ``Input._on_key → event.stop()`` problem that breaks
1644
+ Layer 12's keypress trigger.
1645
+ """
1646
+ self._reenable_mouse_tracking()
1647
+
1648
+ try:
1649
+ self.refresh()
1650
+ except Exception:
1651
+ pass
1652
+
1653
+ try:
1654
+ self.query_one(ChatPane).add_system_message(
1655
+ "[Ctrl+R] Terminal modes re-armed (mouse / focus / paste)"
1656
+ )
1657
+ except Exception:
1658
+ pass
1659
+
1452
1660
  def action_toggle_sidebar(self) -> None:
1453
1661
  """Hide / show the right sidebar to give the chat full width."""
1454
1662
  try:
@@ -423,6 +423,184 @@ the recovery sequence itself can be ineffective; let's make the
423
423
  recovery actually do something". The two stack: tick still runs,
424
424
  keypress trigger gives it sub-second latency, off-on shake makes the
425
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.
426
604
  """
427
605
 
428
606
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.45.0
3
+ Version: 0.47.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
@@ -167,12 +167,14 @@ tests/test_tui_completer_dynamic.py
167
167
  tests/test_tui_copy.py
168
168
  tests/test_tui_input_behaviour.py
169
169
  tests/test_tui_layer12_recovery.py
170
+ tests/test_tui_layer13_recovery.py
170
171
  tests/test_tui_mention_expand.py
171
172
  tests/test_tui_modals.py
172
173
  tests/test_tui_mode_cycle.py
173
174
  tests/test_tui_native_selection.py
174
175
  tests/test_tui_permission_flow.py
175
176
  tests/test_tui_plan_task_render.py
177
+ tests/test_tui_shell_bang.py
176
178
  tests/test_tui_sidebar_toggle.py
177
179
  tests/test_tui_slash_bridge.py
178
180
  tests/test_tui_snapshot_smoke.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.45.0"
7
+ version = "0.47.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"