aru-code 0.55.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.
- {aru_code-0.55.0/aru_code.egg-info → aru_code-0.57.0}/PKG-INFO +1 -1
- aru_code-0.57.0/aru/__init__.py +1 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/display.py +6 -3
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/plan_mode.py +13 -1
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/app.py +35 -4
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/slash_bridge.py +11 -1
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/ui.py +51 -0
- {aru_code-0.55.0 → aru_code-0.57.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.55.0 → aru_code-0.57.0}/pyproject.toml +1 -1
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_plan_mode_refactor.py +54 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_permission_flow.py +145 -0
- aru_code-0.55.0/aru/__init__.py +0 -1
- {aru_code-0.55.0 → aru_code-0.57.0}/LICENSE +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/README.md +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/_debug/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/_debug/analyze_trace.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/_debug/loop_tracer.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/agent_factory.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/agents/base.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/agents/catalog.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/agents/planner.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/cache_patch.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/checkpoints.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/cli.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/commands.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/completers.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/config.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/context.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/doom_loop.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/events.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/format/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/format/manager.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/format/runner.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/history_blocks.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/lsp/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/lsp/client.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/lsp/manager.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/lsp/protocol.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/memory/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/memory/extractor.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/memory/loader.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/memory/store.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/permissions.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/plugin_cache.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/providers.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/runner.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/runtime.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/select.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/session.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/sinks.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/streaming.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tool_policy.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/_diff.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/_shared.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/apply_patch.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/apply_patch_prompt.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/delegate.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/delegate_prompt.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/file_ops.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/lsp.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/memory_tool.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/registry.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/search.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/shell.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/skill.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/web.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tools/worktree.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/log_bridge.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/notifications.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/sanitize.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/choice.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/confirm.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/keymap.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/search.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/session_picker.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/screens/text_input.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/sinks.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/themes.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/__init__.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/chat.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/completer.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/context_pane.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/file_link.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/header.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/inline_choice.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/loaded_pane.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/prompt_area.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/prompt_queue.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/status.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/subagent_panel.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/tasklist_panel.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/thinking.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/tui/widgets/tools.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru/ui.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/setup.cfg +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_agents_md_coverage.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_apply_patch.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_async_tool_permission.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cache_patch_metrics.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cache_patch_stop_reason.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_catalog.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_chat_scrollable.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_codebase.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_config.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_context.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_context_pane.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_cwd_awareness.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_delegate.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_doom_loop.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_events_backward_compat.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_events_schema.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_fork_ctx_concurrency.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_format.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_invoke_skill.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_invoked_skills.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_loaded_pane_path.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_lsp.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_lsp_rename.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_main.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_markdown_to_text.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_mcp_health.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_memory.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_memory_tool.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_microcompact.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_permission_timeout_suspension.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_permissions.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_plugin_cache.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_plugin_errors.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_plugin_hooks_v2.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_plugins.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_providers.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_ranker.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_reasoning.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_runner_interrupt.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_runner_recovery.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_runtime.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_select.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_session_free_cost.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_skill_disallowed_tools.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_status_breakdown.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_status_cost.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_streaming_sink.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_subagent_tool_events.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tasklist.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_thread_tool_timeout.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tool_policy.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_truncation_marker.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_app_boot.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_bindings.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_bus_flow.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_chat.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_chat_adversarial.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_completer.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_completer_dynamic.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_copy.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_error_display.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_file_link.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_input_behaviour.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_layer12_recovery.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_layer13_recovery.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_mention_expand.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_modals.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_mode_cycle.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_native_selection.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_plan_task_render.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_prompt_queue.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_shell_bang.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_sidebar_toggle.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_slash_bridge.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_slash_model.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_snapshot_smoke.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_subagent_panel.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_theme.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_thinking_and_boot.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_tui_widgets_visual.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_ui_adapter.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_worktree.py +0 -0
- {aru_code-0.55.0 → aru_code-0.57.0}/tests/test_worktree_session_restore.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.57.0"
|
|
@@ -301,13 +301,16 @@ class ToolTracker:
|
|
|
301
301
|
self._completed: list[tuple[str, float]] = [] # (label, duration)
|
|
302
302
|
|
|
303
303
|
def start(self, tool_id: str, label: str):
|
|
304
|
-
|
|
304
|
+
# perf_counter, not monotonic: on Windows monotonic() has ~15.6ms
|
|
305
|
+
# resolution, so a sub-tick tool call would report a 0.0 duration.
|
|
306
|
+
# perf_counter is monotonic too but high-resolution (~100ns).
|
|
307
|
+
self._active[tool_id] = (label, time.perf_counter())
|
|
305
308
|
|
|
306
309
|
def complete(self, tool_id: str) -> tuple[str, float] | None:
|
|
307
310
|
entry = self._active.pop(tool_id, None)
|
|
308
311
|
if entry:
|
|
309
312
|
label, start = entry
|
|
310
|
-
duration = time.
|
|
313
|
+
duration = time.perf_counter() - start
|
|
311
314
|
self._completed.append((label, duration))
|
|
312
315
|
return label, duration
|
|
313
316
|
return None
|
|
@@ -315,7 +318,7 @@ class ToolTracker:
|
|
|
315
318
|
@property
|
|
316
319
|
def active_labels(self) -> list[tuple[str, float]]:
|
|
317
320
|
"""Return (label, elapsed_seconds) for each active tool."""
|
|
318
|
-
now = time.
|
|
321
|
+
now = time.perf_counter()
|
|
319
322
|
return [(label, now - start) for label, start in self._active.values()]
|
|
320
323
|
|
|
321
324
|
def pop_completed(self) -> list[tuple[str, float]]:
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -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
|
|
|
@@ -9,6 +9,33 @@ import pytest
|
|
|
9
9
|
pytest.importorskip("textual")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
async def _wait_for_inline_focus(app, pilot, *, max_iter: int = 50):
|
|
13
|
+
"""Block until the InlineChoicePrompt's OptionList owns focus.
|
|
14
|
+
|
|
15
|
+
``query(InlineChoicePrompt)`` returns the widget as soon as it is in
|
|
16
|
+
the DOM, but the OptionList only gains focus (and its default
|
|
17
|
+
highlight) inside ``InlineChoicePrompt.on_mount`` — a message
|
|
18
|
+
dispatched *after* mount completes. Pressing Enter / arrow keys
|
|
19
|
+
before that lands sends the key nowhere useful: ``OptionSelected``
|
|
20
|
+
never fires and the worker thread blocks until its timeout. In
|
|
21
|
+
isolation the gap is sub-tick so the press always lands; under a
|
|
22
|
+
loaded suite the mount lifecycle slips behind detection and the test
|
|
23
|
+
flakes with a TimeoutError. Waiting for focus closes the race.
|
|
24
|
+
"""
|
|
25
|
+
from textual.widgets import OptionList
|
|
26
|
+
|
|
27
|
+
from aru.tui.widgets.inline_choice import InlineChoicePrompt
|
|
28
|
+
|
|
29
|
+
for _ in range(max_iter):
|
|
30
|
+
prompts = list(app.query(InlineChoicePrompt))
|
|
31
|
+
if prompts:
|
|
32
|
+
opts = list(prompts[0].query(OptionList))
|
|
33
|
+
if opts and app.focused is opts[0] and opts[0].highlighted is not None:
|
|
34
|
+
return opts[0]
|
|
35
|
+
await pilot.pause(0.05)
|
|
36
|
+
raise AssertionError("InlineChoicePrompt OptionList never took focus")
|
|
37
|
+
|
|
38
|
+
|
|
12
39
|
@pytest.mark.asyncio
|
|
13
40
|
async def test_tui_ask_choice_from_worker_resolves_via_modal():
|
|
14
41
|
"""TuiUI.ask_choice invoked from a worker thread returns modal result.
|
|
@@ -106,6 +133,9 @@ async def test_ask_choice_with_details_uses_inline_prompt_not_modal():
|
|
|
106
133
|
"expected InlineChoicePrompt in ChatPane"
|
|
107
134
|
)
|
|
108
135
|
# Press Enter — OptionList focuses on mount, default=0 highlighted.
|
|
136
|
+
# Wait for that focus to actually land first; querying the prompt
|
|
137
|
+
# only proves it is in the DOM (see _wait_for_inline_focus).
|
|
138
|
+
await _wait_for_inline_focus(app, pilot)
|
|
109
139
|
await pilot.press("enter")
|
|
110
140
|
await asyncio.wait_for(task, timeout=5.0)
|
|
111
141
|
assert holder["choice"] == 0
|
|
@@ -156,6 +186,7 @@ async def test_inline_prompt_hides_input_bar_and_restores_on_answer():
|
|
|
156
186
|
assert inp.has_class("-hidden"), (
|
|
157
187
|
"input should be hidden while InlineChoicePrompt is mounted"
|
|
158
188
|
)
|
|
189
|
+
await _wait_for_inline_focus(app, pilot)
|
|
159
190
|
await pilot.press("enter")
|
|
160
191
|
await asyncio.wait_for(task, timeout=5.0)
|
|
161
192
|
# After the user answers, the input bar is restored.
|
|
@@ -199,6 +230,7 @@ async def test_ask_choice_inline_esc_cancels_with_cancel_value():
|
|
|
199
230
|
await pilot.pause(0.05)
|
|
200
231
|
if list(app.query_one(ChatPane).query(InlineChoicePrompt)):
|
|
201
232
|
break
|
|
233
|
+
await _wait_for_inline_focus(app, pilot)
|
|
202
234
|
await pilot.press("escape")
|
|
203
235
|
await asyncio.wait_for(task, timeout=5.0)
|
|
204
236
|
assert holder["choice"] == 99
|
|
@@ -257,6 +289,7 @@ async def test_auto_accept_inline_choice_updates_status_pane_mode():
|
|
|
257
289
|
if list(chat.query(InlineChoicePrompt)):
|
|
258
290
|
break
|
|
259
291
|
# Option index 1 = "Yes, and auto-accept edits".
|
|
292
|
+
await _wait_for_inline_focus(app, pilot)
|
|
260
293
|
await pilot.press("down")
|
|
261
294
|
await pilot.press("enter")
|
|
262
295
|
await asyncio.wait_for(task, timeout=5.0)
|
|
@@ -320,6 +353,7 @@ async def test_thinking_spinner_hidden_while_prompt_open():
|
|
|
320
353
|
"spinner must be hidden while the permission prompt is open"
|
|
321
354
|
)
|
|
322
355
|
|
|
356
|
+
await _wait_for_inline_focus(app, pilot)
|
|
323
357
|
await pilot.press("enter")
|
|
324
358
|
await asyncio.wait_for(task, timeout=5.0)
|
|
325
359
|
for _ in range(20):
|
|
@@ -332,6 +366,53 @@ async def test_thinking_spinner_hidden_while_prompt_open():
|
|
|
332
366
|
assert holder["choice"] == 0
|
|
333
367
|
|
|
334
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
|
+
|
|
335
416
|
@pytest.mark.asyncio
|
|
336
417
|
async def test_tui_confirm_from_worker_returns_bool():
|
|
337
418
|
from aru.tui.app import AruApp
|
|
@@ -356,3 +437,67 @@ async def test_tui_confirm_from_worker_returns_bool():
|
|
|
356
437
|
await pilot.press("y")
|
|
357
438
|
await asyncio.wait_for(worker_task, timeout=5.0)
|
|
358
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
|
+
)
|
aru_code-0.55.0/aru/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.55.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
|
|
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
|