aru-code 0.56.0__tar.gz → 0.57.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 (212) hide show
  1. {aru_code-0.56.0/aru_code.egg-info → aru_code-0.57.0}/PKG-INFO +1 -1
  2. aru_code-0.57.0/aru/__init__.py +1 -0
  3. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/plan_mode.py +13 -1
  4. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/app.py +35 -4
  5. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/slash_bridge.py +11 -1
  6. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/ui.py +51 -0
  7. {aru_code-0.56.0 → aru_code-0.57.0/aru_code.egg-info}/PKG-INFO +1 -1
  8. {aru_code-0.56.0 → aru_code-0.57.0}/pyproject.toml +1 -1
  9. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_plan_mode_refactor.py +54 -0
  10. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_permission_flow.py +111 -0
  11. aru_code-0.56.0/aru/__init__.py +0 -1
  12. {aru_code-0.56.0 → aru_code-0.57.0}/LICENSE +0 -0
  13. {aru_code-0.56.0 → aru_code-0.57.0}/README.md +0 -0
  14. {aru_code-0.56.0 → aru_code-0.57.0}/aru/_debug/__init__.py +0 -0
  15. {aru_code-0.56.0 → aru_code-0.57.0}/aru/_debug/analyze_trace.py +0 -0
  16. {aru_code-0.56.0 → aru_code-0.57.0}/aru/_debug/loop_tracer.py +0 -0
  17. {aru_code-0.56.0 → aru_code-0.57.0}/aru/agent_factory.py +0 -0
  18. {aru_code-0.56.0 → aru_code-0.57.0}/aru/agents/__init__.py +0 -0
  19. {aru_code-0.56.0 → aru_code-0.57.0}/aru/agents/base.py +0 -0
  20. {aru_code-0.56.0 → aru_code-0.57.0}/aru/agents/catalog.py +0 -0
  21. {aru_code-0.56.0 → aru_code-0.57.0}/aru/agents/planner.py +0 -0
  22. {aru_code-0.56.0 → aru_code-0.57.0}/aru/cache_patch.py +0 -0
  23. {aru_code-0.56.0 → aru_code-0.57.0}/aru/checkpoints.py +0 -0
  24. {aru_code-0.56.0 → aru_code-0.57.0}/aru/cli.py +0 -0
  25. {aru_code-0.56.0 → aru_code-0.57.0}/aru/commands.py +0 -0
  26. {aru_code-0.56.0 → aru_code-0.57.0}/aru/completers.py +0 -0
  27. {aru_code-0.56.0 → aru_code-0.57.0}/aru/config.py +0 -0
  28. {aru_code-0.56.0 → aru_code-0.57.0}/aru/context.py +0 -0
  29. {aru_code-0.56.0 → aru_code-0.57.0}/aru/display.py +0 -0
  30. {aru_code-0.56.0 → aru_code-0.57.0}/aru/doom_loop.py +0 -0
  31. {aru_code-0.56.0 → aru_code-0.57.0}/aru/events.py +0 -0
  32. {aru_code-0.56.0 → aru_code-0.57.0}/aru/format/__init__.py +0 -0
  33. {aru_code-0.56.0 → aru_code-0.57.0}/aru/format/manager.py +0 -0
  34. {aru_code-0.56.0 → aru_code-0.57.0}/aru/format/runner.py +0 -0
  35. {aru_code-0.56.0 → aru_code-0.57.0}/aru/history_blocks.py +0 -0
  36. {aru_code-0.56.0 → aru_code-0.57.0}/aru/lsp/__init__.py +0 -0
  37. {aru_code-0.56.0 → aru_code-0.57.0}/aru/lsp/client.py +0 -0
  38. {aru_code-0.56.0 → aru_code-0.57.0}/aru/lsp/manager.py +0 -0
  39. {aru_code-0.56.0 → aru_code-0.57.0}/aru/lsp/protocol.py +0 -0
  40. {aru_code-0.56.0 → aru_code-0.57.0}/aru/memory/__init__.py +0 -0
  41. {aru_code-0.56.0 → aru_code-0.57.0}/aru/memory/extractor.py +0 -0
  42. {aru_code-0.56.0 → aru_code-0.57.0}/aru/memory/loader.py +0 -0
  43. {aru_code-0.56.0 → aru_code-0.57.0}/aru/memory/store.py +0 -0
  44. {aru_code-0.56.0 → aru_code-0.57.0}/aru/permissions.py +0 -0
  45. {aru_code-0.56.0 → aru_code-0.57.0}/aru/plugin_cache.py +0 -0
  46. {aru_code-0.56.0 → aru_code-0.57.0}/aru/plugins/__init__.py +0 -0
  47. {aru_code-0.56.0 → aru_code-0.57.0}/aru/plugins/custom_tools.py +0 -0
  48. {aru_code-0.56.0 → aru_code-0.57.0}/aru/plugins/hooks.py +0 -0
  49. {aru_code-0.56.0 → aru_code-0.57.0}/aru/plugins/manager.py +0 -0
  50. {aru_code-0.56.0 → aru_code-0.57.0}/aru/plugins/tool_api.py +0 -0
  51. {aru_code-0.56.0 → aru_code-0.57.0}/aru/providers.py +0 -0
  52. {aru_code-0.56.0 → aru_code-0.57.0}/aru/runner.py +0 -0
  53. {aru_code-0.56.0 → aru_code-0.57.0}/aru/runtime.py +0 -0
  54. {aru_code-0.56.0 → aru_code-0.57.0}/aru/select.py +0 -0
  55. {aru_code-0.56.0 → aru_code-0.57.0}/aru/session.py +0 -0
  56. {aru_code-0.56.0 → aru_code-0.57.0}/aru/sinks.py +0 -0
  57. {aru_code-0.56.0 → aru_code-0.57.0}/aru/streaming.py +0 -0
  58. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tool_policy.py +0 -0
  59. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/__init__.py +0 -0
  60. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/_diff.py +0 -0
  61. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/_shared.py +0 -0
  62. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/apply_patch.py +0 -0
  63. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/apply_patch_prompt.txt +0 -0
  64. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/ast_tools.py +0 -0
  65. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/codebase.py +0 -0
  66. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/delegate.py +0 -0
  67. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/delegate_prompt.txt +0 -0
  68. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/file_ops.py +0 -0
  69. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/gitignore.py +0 -0
  70. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/lsp.py +0 -0
  71. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/mcp_client.py +0 -0
  72. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/memory_tool.py +0 -0
  73. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/ranker.py +0 -0
  74. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/registry.py +0 -0
  75. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/search.py +0 -0
  76. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/shell.py +0 -0
  77. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/skill.py +0 -0
  78. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/tasklist.py +0 -0
  79. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/web.py +0 -0
  80. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tools/worktree.py +0 -0
  81. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/__init__.py +0 -0
  82. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/log_bridge.py +0 -0
  83. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/notifications.py +0 -0
  84. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/sanitize.py +0 -0
  85. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/__init__.py +0 -0
  86. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/choice.py +0 -0
  87. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/confirm.py +0 -0
  88. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/keymap.py +0 -0
  89. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/search.py +0 -0
  90. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/session_picker.py +0 -0
  91. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/screens/text_input.py +0 -0
  92. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/sinks.py +0 -0
  93. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/themes.py +0 -0
  94. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/__init__.py +0 -0
  95. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/chat.py +0 -0
  96. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/completer.py +0 -0
  97. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/context_pane.py +0 -0
  98. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/file_link.py +0 -0
  99. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/header.py +0 -0
  100. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/inline_choice.py +0 -0
  101. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/loaded_pane.py +0 -0
  102. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/prompt_area.py +0 -0
  103. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/prompt_queue.py +0 -0
  104. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/status.py +0 -0
  105. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/subagent_panel.py +0 -0
  106. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/tasklist_panel.py +0 -0
  107. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/thinking.py +0 -0
  108. {aru_code-0.56.0 → aru_code-0.57.0}/aru/tui/widgets/tools.py +0 -0
  109. {aru_code-0.56.0 → aru_code-0.57.0}/aru/ui.py +0 -0
  110. {aru_code-0.56.0 → aru_code-0.57.0}/aru_code.egg-info/SOURCES.txt +0 -0
  111. {aru_code-0.56.0 → aru_code-0.57.0}/aru_code.egg-info/dependency_links.txt +0 -0
  112. {aru_code-0.56.0 → aru_code-0.57.0}/aru_code.egg-info/entry_points.txt +0 -0
  113. {aru_code-0.56.0 → aru_code-0.57.0}/aru_code.egg-info/requires.txt +0 -0
  114. {aru_code-0.56.0 → aru_code-0.57.0}/aru_code.egg-info/top_level.txt +0 -0
  115. {aru_code-0.56.0 → aru_code-0.57.0}/setup.cfg +0 -0
  116. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_agents_base.py +0 -0
  117. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_agents_md_coverage.py +0 -0
  118. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_apply_patch.py +0 -0
  119. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_async_tool_permission.py +0 -0
  120. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cache_patch_metrics.py +0 -0
  121. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cache_patch_stop_reason.py +0 -0
  122. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_catalog.py +0 -0
  123. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_chat_scrollable.py +0 -0
  124. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_checkpoints.py +0 -0
  125. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli.py +0 -0
  126. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_advanced.py +0 -0
  127. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_base.py +0 -0
  128. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_completers.py +0 -0
  129. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_new.py +0 -0
  130. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_run_cli.py +0 -0
  131. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_session.py +0 -0
  132. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cli_shell.py +0 -0
  133. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_codebase.py +0 -0
  134. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_confabulation_regression.py +0 -0
  135. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_config.py +0 -0
  136. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_context.py +0 -0
  137. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_context_pane.py +0 -0
  138. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_cwd_awareness.py +0 -0
  139. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_delegate.py +0 -0
  140. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_doom_loop.py +0 -0
  141. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_events_backward_compat.py +0 -0
  142. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_events_schema.py +0 -0
  143. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_fork_ctx_concurrency.py +0 -0
  144. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_format.py +0 -0
  145. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_gitignore.py +0 -0
  146. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_guardrails_scenarios.py +0 -0
  147. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_invoke_skill.py +0 -0
  148. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_invoked_skills.py +0 -0
  149. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_loaded_pane_path.py +0 -0
  150. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_lsp.py +0 -0
  151. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_lsp_rename.py +0 -0
  152. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_main.py +0 -0
  153. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_markdown_to_text.py +0 -0
  154. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_mcp_client.py +0 -0
  155. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_mcp_health.py +0 -0
  156. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_memory.py +0 -0
  157. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_memory_tool.py +0 -0
  158. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_microcompact.py +0 -0
  159. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_permission_timeout_suspension.py +0 -0
  160. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_permissions.py +0 -0
  161. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_plugin_cache.py +0 -0
  162. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_plugin_errors.py +0 -0
  163. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_plugin_hooks_v2.py +0 -0
  164. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_plugins.py +0 -0
  165. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_providers.py +0 -0
  166. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_ranker.py +0 -0
  167. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_reasoning.py +0 -0
  168. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_runner_interrupt.py +0 -0
  169. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_runner_recovery.py +0 -0
  170. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_runtime.py +0 -0
  171. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_select.py +0 -0
  172. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_session_free_cost.py +0 -0
  173. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_skill_disallowed_tools.py +0 -0
  174. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_status_breakdown.py +0 -0
  175. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_status_cost.py +0 -0
  176. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_streaming_sink.py +0 -0
  177. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_subagent_tool_events.py +0 -0
  178. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tasklist.py +0 -0
  179. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_thread_tool_timeout.py +0 -0
  180. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tool_policy.py +0 -0
  181. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_truncation_marker.py +0 -0
  182. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_app_boot.py +0 -0
  183. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_bindings.py +0 -0
  184. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_bus_flow.py +0 -0
  185. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_chat.py +0 -0
  186. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_chat_adversarial.py +0 -0
  187. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_completer.py +0 -0
  188. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_completer_dynamic.py +0 -0
  189. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_copy.py +0 -0
  190. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_error_display.py +0 -0
  191. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_file_link.py +0 -0
  192. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_input_behaviour.py +0 -0
  193. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_layer12_recovery.py +0 -0
  194. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_layer13_recovery.py +0 -0
  195. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_mention_expand.py +0 -0
  196. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_modals.py +0 -0
  197. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_mode_cycle.py +0 -0
  198. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_native_selection.py +0 -0
  199. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_plan_task_render.py +0 -0
  200. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_prompt_queue.py +0 -0
  201. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_shell_bang.py +0 -0
  202. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_sidebar_toggle.py +0 -0
  203. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_slash_bridge.py +0 -0
  204. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_slash_model.py +0 -0
  205. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_snapshot_smoke.py +0 -0
  206. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_subagent_panel.py +0 -0
  207. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_theme.py +0 -0
  208. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_thinking_and_boot.py +0 -0
  209. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_tui_widgets_visual.py +0 -0
  210. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_ui_adapter.py +0 -0
  211. {aru_code-0.56.0 → aru_code-0.57.0}/tests/test_worktree.py +0 -0
  212. {aru_code-0.56.0 → aru_code-0.57.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.56.0
3
+ Version: 0.57.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.57.0"
@@ -24,6 +24,7 @@ specialized read-only tool set and instructions.
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
+ import asyncio
27
28
  import sys
28
29
 
29
30
  from rich.panel import Panel
@@ -197,7 +198,18 @@ async def exit_plan_mode(plan: str) -> str:
197
198
  session.set_plan(task=task_label, plan_content=plan_text)
198
199
  n_steps = len(session.plan_steps)
199
200
 
200
- approved, feedback = _prompt_plan_approval(session.plan_steps, n_steps)
201
+ # ``exit_plan_mode`` is an async tool, so ``agent_factory`` awaits it
202
+ # directly on the event-loop thread (no ``_thread_tool`` worker hop like
203
+ # sync tools get). ``_prompt_plan_approval`` is synchronous and in TUI
204
+ # mode invokes ``TuiUI.ask_choice`` → ``App.call_from_thread``, which
205
+ # raises "must run in a different thread from the app" when called from
206
+ # the loop thread it targets. Hop to a worker thread first so the modal
207
+ # dispatch path matches the sync-tool and runner-auto-exit paths
208
+ # (``runner.py`` does the same). ``asyncio.to_thread`` copies the
209
+ # contextvars snapshot, so ``get_ctx()`` inside still resolves.
210
+ approved, feedback = await asyncio.to_thread(
211
+ _prompt_plan_approval, session.plan_steps, n_steps
212
+ )
201
213
 
202
214
  # The approval prompt already rendered the plan panel inline, so suppress
203
215
  # the runner's coalesced end-of-batch render to avoid a duplicate.
@@ -873,14 +873,45 @@ class AruApp(App):
873
873
  return False
874
874
 
875
875
  def _run_bridged_slash(self, name: str, body: str) -> None:
876
- """Execute a bridged REPL handler and show its output in ChatPane."""
876
+ """Execute a bridged REPL handler and show its output in ChatPane.
877
+
878
+ Dispatched as a coroutine worker so the handler runs OFF the
879
+ event-loop thread. Some bridged handlers prompt — ``/memory clear``
880
+ calls ``ctx.ui.confirm``, which bridges to the App loop via
881
+ ``App.call_from_thread`` and therefore *must* be invoked from a
882
+ thread other than the loop (otherwise it raises, or degrades to the
883
+ cancel default — see ``TuiUI._on_app_thread``). The worker hops the
884
+ handler to a thread via ``asyncio.to_thread`` so the loop stays free
885
+ to draw and service the modal, then marshals the captured output
886
+ back to the ChatPane.
887
+ """
888
+ from aru.tui.slash_bridge import BRIDGED_COMMANDS
889
+
890
+ if name.lower() not in BRIDGED_COMMANDS:
891
+ return
892
+ self.run_worker(
893
+ self._run_bridged_slash_async(name, body),
894
+ name=f"slash-{name}",
895
+ group="slash-bridge",
896
+ exclusive=False,
897
+ )
898
+
899
+ async def _run_bridged_slash_async(self, name: str, body: str) -> None:
900
+ """Worker body for :meth:`_run_bridged_slash` — see its docstring."""
877
901
  from aru.tui.slash_bridge import run_bridged
878
902
 
879
- handled, text = run_bridged(name, body, self)
903
+ # Hop to a worker thread: the handler may block on a ``ctx.ui``
904
+ # prompt whose ModalScreen dispatch needs the loop to stay
905
+ # responsive. ``asyncio.to_thread`` copies the contextvars snapshot
906
+ # so the handler's ``get_ctx()`` still resolves.
907
+ handled, text = await asyncio.to_thread(run_bridged, name, body, self)
880
908
  if not handled:
881
909
  return
882
- chat = self.query_one(ChatPane)
883
- # Prefix with the command so the user has context.
910
+ # Back on the loop after the await — safe to touch widgets.
911
+ try:
912
+ chat = self.query_one(ChatPane)
913
+ except Exception:
914
+ return
884
915
  header = f"/{name}" + (f" {body}" if body else "")
885
916
  chat.add_system_message(f"$ {header}\n{text}" if text else f"$ {header}")
886
917
 
@@ -119,7 +119,17 @@ def run_bridged(name: str, body: str, app: Any) -> tuple[bool, str]:
119
119
 
120
120
  import aru.commands as cmds_module
121
121
  original_console = cmds_module.console
122
- temp = Console(record=True, width=100, force_terminal=True, color_system=None)
122
+ # ``file=StringIO()`` keeps the live output off the raw terminal — we
123
+ # only want the recorded copy (``export_text`` below), which we mirror
124
+ # into the ChatPane. Writing to stdout here is pointless under Textual
125
+ # (it owns the screen) and outright unsafe now that the handler runs on
126
+ # a worker thread; ``record=True`` still captures everything regardless
127
+ # of the file target.
128
+ import io
129
+ temp = Console(
130
+ record=True, width=100, force_terminal=True, color_system=None,
131
+ file=io.StringIO(),
132
+ )
123
133
  cmds_module.console = temp
124
134
  try:
125
135
  handler(*args, **kwargs)
@@ -23,10 +23,14 @@ Contract notes:
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ import asyncio
27
+ import logging
26
28
  from typing import Any, Sequence
27
29
 
28
30
  from aru.tui.screens import ChoiceModal, ConfirmModal, TextInputModal
29
31
 
32
+ _log = logging.getLogger("aru.tui.ui")
33
+
30
34
 
31
35
  class TuiUI:
32
36
  """UIAdapter backed by Textual ModalScreens."""
@@ -34,6 +38,28 @@ class TuiUI:
34
38
  def __init__(self, app: Any) -> None:
35
39
  self.app = app
36
40
 
41
+ @staticmethod
42
+ def _on_app_thread() -> bool:
43
+ """True when the caller is running on an asyncio event-loop thread.
44
+
45
+ ``TuiUI``'s blocking prompts bridge to the App loop via
46
+ ``App.call_from_thread``, which *requires* the caller to be on a
47
+ DIFFERENT thread than the loop (otherwise it raises rather than
48
+ deadlock). The correct callers reach us from ``asyncio.to_thread`` /
49
+ ``_thread_tool`` workers — plain threads with no running loop. If a
50
+ loop IS running in this thread, we are on the App's own event-loop
51
+ thread and must not block: that would freeze the loop so the prompt
52
+ could never be drawn or answered. The guards below degrade safely in
53
+ that case instead of surfacing the cryptic ``call_from_thread``
54
+ ``RuntimeError``. The real fix for any such caller is to wrap the
55
+ call in ``await asyncio.to_thread(ctx.ui.<method>, ...)``.
56
+ """
57
+ try:
58
+ asyncio.get_running_loop()
59
+ return True
60
+ except RuntimeError:
61
+ return False
62
+
37
63
  # ── choice ────────────────────────────────────────────────────────
38
64
 
39
65
  def ask_choice(
@@ -88,6 +114,19 @@ class TuiUI:
88
114
  user answers, mirroring ``_run_modal`` so the sync call sites
89
115
  (``check_permission``, plan approval) keep their contract.
90
116
  """
117
+ if self._on_app_thread():
118
+ # Contract violation (see ``_on_app_thread``): a caller dispatched
119
+ # this blocking prompt from the App's own event-loop thread. We
120
+ # cannot show it here, so degrade to ``cancel_value`` rather than
121
+ # crash the turn with ``call_from_thread``'s RuntimeError.
122
+ _log.warning(
123
+ "TuiUI.ask_choice invoked on the event-loop thread; returning "
124
+ "cancel_value=%r without prompting. Wrap the call site in "
125
+ "asyncio.to_thread.",
126
+ cancel_value,
127
+ )
128
+ return cancel_value
129
+
91
130
  import threading
92
131
 
93
132
  from aru.tui.widgets.chat import ChatPane
@@ -252,6 +291,18 @@ class TuiUI:
252
291
  we work without an active Textual worker context. Designed to be
253
292
  called from ``asyncio.to_thread`` tool threads.
254
293
  """
294
+ if self._on_app_thread():
295
+ # See ``_on_app_thread``: blocking on the loop thread would
296
+ # freeze the App. Degrade to ``None`` (callers map this to their
297
+ # default / cancel outcome) instead of raising.
298
+ _log.warning(
299
+ "TuiUI modal (%s) invoked on the event-loop thread; "
300
+ "dismissing without prompting. Wrap the call site in "
301
+ "asyncio.to_thread.",
302
+ type(modal).__name__,
303
+ )
304
+ return None
305
+
255
306
  import threading
256
307
 
257
308
  done = threading.Event()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.56.0
3
+ Version: 0.57.0
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.56.0"
7
+ version = "0.57.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -245,6 +245,60 @@ class TestExitPlanMode:
245
245
  assert "needs more detail" in result
246
246
  assert "STILL in plan mode" in result
247
247
 
248
+ @pytest.mark.asyncio
249
+ async def test_approval_prompt_runs_off_event_loop_thread(self):
250
+ """Regression: the approval prompt must run on a worker thread, never
251
+ the event-loop thread.
252
+
253
+ ``exit_plan_mode`` is an async tool awaited directly on the loop. In
254
+ TUI mode ``_prompt_plan_approval`` reaches ``TuiUI.ask_choice`` →
255
+ ``App.call_from_thread``, which raises "must run in a different
256
+ thread from the app" when called on the loop thread. The fix hops to
257
+ a worker via ``asyncio.to_thread``; this test asserts the prompt
258
+ observes a thread id different from the loop's. Before the fix the
259
+ ids matched and TUI users hit the RuntimeError.
260
+ """
261
+ import threading
262
+
263
+ from aru.runtime import RuntimeContext, _runtime_ctx as _ctx
264
+ from aru.tools import plan_mode as plan_mode_module
265
+
266
+ loop_thread_id = threading.get_ident()
267
+ captured: dict = {}
268
+
269
+ def _fake_prompt(plan_steps, n_steps):
270
+ # Runs inside exit_plan_mode's approval call. Record the thread.
271
+ captured["thread_id"] = threading.get_ident()
272
+ # Also prove get_ctx() still resolves across the to_thread hop.
273
+ from aru.runtime import get_ctx
274
+ captured["has_ctx_session"] = get_ctx().session is not None
275
+ return (True, "")
276
+
277
+ session = Session()
278
+ session.plan_mode = True
279
+ ctx = RuntimeContext()
280
+ ctx.session = session
281
+ token = _ctx.set(ctx)
282
+ with patch.object(plan_mode_module, "_prompt_plan_approval", _fake_prompt):
283
+ try:
284
+ result = await plan_mode_module.exit_plan_mode(
285
+ plan="## Steps\n1. do thing\n2. other thing"
286
+ )
287
+ finally:
288
+ _ctx.reset(token)
289
+
290
+ assert "thread_id" in captured, "approval prompt was never invoked"
291
+ assert captured["thread_id"] != loop_thread_id, (
292
+ "approval prompt ran on the event-loop thread — "
293
+ "TuiUI.ask_choice's call_from_thread would raise in TUI mode"
294
+ )
295
+ assert captured["has_ctx_session"], (
296
+ "contextvars must propagate across asyncio.to_thread so the "
297
+ "prompt can still read ctx.session"
298
+ )
299
+ assert session.plan_mode is False
300
+ assert "approved" in result.lower()
301
+
248
302
 
249
303
  # ── Tool-wrapper plan-mode gate ───────────────────────────────────────
250
304
 
@@ -366,6 +366,53 @@ async def test_thinking_spinner_hidden_while_prompt_open():
366
366
  assert holder["choice"] == 0
367
367
 
368
368
 
369
+ @pytest.mark.asyncio
370
+ async def test_blocking_prompt_on_event_loop_thread_degrades_not_raises():
371
+ """Defense-in-depth (bug class): a blocking TuiUI prompt invoked directly
372
+ on the App's event-loop thread must NOT raise the cryptic
373
+ 'call_from_thread must run in a different thread' RuntimeError.
374
+
375
+ The correct fix for any *known* caller is to hop to a worker thread
376
+ (asyncio.to_thread) — this guard is the safety net that keeps a stray
377
+ on-loop call (e.g. the /memory clear slash bridge) from crashing a turn.
378
+ It degrades to cancel_value / the caller default instead.
379
+ """
380
+ from rich.panel import Panel
381
+
382
+ from aru.tui.ui import TuiUI
383
+
384
+ class _DummyApp:
385
+ # If the guard fails, _run_inline_choice/_run_modal would reach this
386
+ # and we'd see the assertion instead of a clean degrade.
387
+ def call_from_thread(self, *args, **kwargs):
388
+ raise AssertionError(
389
+ "call_from_thread must not be reached on the loop thread"
390
+ )
391
+
392
+ ui = TuiUI(_DummyApp())
393
+ # This coroutine runs on the event loop, so we ARE on the loop thread.
394
+ assert TuiUI._on_app_thread() is True
395
+
396
+ # Inline path (details present) — the exact shape of the plan-approval /
397
+ # edit-permission prompt. Degrades to cancel_value.
398
+ inline = ui.ask_choice(
399
+ ["Yes", "No"],
400
+ title="Approve?",
401
+ default=0,
402
+ cancel_value=99,
403
+ details=Panel("preview"),
404
+ )
405
+ assert inline == 99
406
+
407
+ # Modal path (no details) — degrades to None.
408
+ modal = ui.ask_choice(["Yes", "No"], title="Pick", default=0, cancel_value=None)
409
+ assert modal is None
410
+
411
+ # confirm() maps the degraded None back to the caller's default.
412
+ assert ui.confirm("Proceed?", default=True) is True
413
+ assert ui.confirm("Proceed?", default=False) is False
414
+
415
+
369
416
  @pytest.mark.asyncio
370
417
  async def test_tui_confirm_from_worker_returns_bool():
371
418
  from aru.tui.app import AruApp
@@ -390,3 +437,67 @@ async def test_tui_confirm_from_worker_returns_bool():
390
437
  await pilot.press("y")
391
438
  await asyncio.wait_for(worker_task, timeout=5.0)
392
439
  assert result_holder["answer"] is True
440
+
441
+
442
+ @pytest.mark.asyncio
443
+ async def test_memory_clear_slash_shows_confirm_modal_and_clears():
444
+ """Regression: ``/memory clear`` in the TUI must actually show the
445
+ ConfirmModal (and clear on "yes"), not silently degrade.
446
+
447
+ The slash bridge used to run handlers inline on the event-loop thread,
448
+ so ``handle_memory_command`` → ``ui.confirm`` → ``call_from_thread``
449
+ raised / degraded to the cancel default and the modal never appeared.
450
+ The bridge now hops the handler to a worker thread (``_run_bridged_slash``
451
+ → ``asyncio.to_thread``), so the modal is dispatched correctly and the
452
+ user's answer is honoured.
453
+ """
454
+ from unittest.mock import patch
455
+
456
+ from aru.runtime import init_ctx, set_ctx
457
+ from aru.session import Session
458
+ from aru.tui.app import AruApp
459
+ from aru.tui.screens import ConfirmModal
460
+ from aru.tui.ui import TuiUI
461
+
462
+ ctx = init_ctx()
463
+ session = Session()
464
+ session.project_root = "/tmp/aru-memory-clear-test"
465
+ app = AruApp(ctx=ctx, session=session)
466
+ ctx.tui_app = app
467
+
468
+ cleared: dict = {}
469
+
470
+ def _fake_clear(project_root, *args, **kwargs):
471
+ cleared["root"] = project_root
472
+ return 3
473
+
474
+ async with app.run_test() as pilot:
475
+ await pilot.pause()
476
+ ctx.ui = TuiUI(app)
477
+ set_ctx(ctx) # ensure the worker task + its to_thread child inherit ctx
478
+ # Patch the destructive clear so the test never touches real memory;
479
+ # the patch stays active across the awaits below, covering the
480
+ # handler's execution on the worker thread.
481
+ with patch("aru.memory.store.clear_memory", _fake_clear):
482
+ app._run_bridged_slash("memory", "clear")
483
+ # The crux of the fix: the modal actually appears. Before the
484
+ # fix the handler ran on the loop thread and degraded silently.
485
+ for _ in range(60):
486
+ await pilot.pause(0.05)
487
+ if app.screen_stack and isinstance(app.screen, ConfirmModal):
488
+ break
489
+ assert isinstance(app.screen, ConfirmModal), (
490
+ "ConfirmModal never appeared — /memory clear degraded instead "
491
+ "of prompting (handler ran on the event-loop thread)"
492
+ )
493
+ # Answer "yes" and let the worker finish + run the (patched) clear.
494
+ await pilot.press("y")
495
+ for _ in range(60):
496
+ await pilot.pause(0.05)
497
+ if "root" in cleared:
498
+ break
499
+
500
+ assert "root" in cleared, (
501
+ "clear_memory was never called — the 'yes' answer did not propagate "
502
+ "back through the worker to the handler"
503
+ )
@@ -1 +0,0 @@
1
- __version__ = "0.56.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