aru-code 0.46.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.
- {aru_code-0.46.0/aru_code.egg-info → aru_code-0.47.0}/PKG-INFO +1 -1
- aru_code-0.47.0/aru/__init__.py +1 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/app.py +137 -0
- {aru_code-0.46.0 → aru_code-0.47.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.46.0 → aru_code-0.47.0}/aru_code.egg-info/SOURCES.txt +1 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/pyproject.toml +1 -1
- aru_code-0.47.0/tests/test_tui_shell_bang.py +183 -0
- aru_code-0.46.0/aru/__init__.py +0 -1
- {aru_code-0.46.0 → aru_code-0.47.0}/LICENSE +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/README.md +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/agent_factory.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/agents/base.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/agents/catalog.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/agents/planner.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/cache_patch.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/checkpoints.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/cli.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/commands.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/completers.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/config.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/context.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/display.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/events.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/format/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/format/manager.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/format/runner.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/history_blocks.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/lsp/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/lsp/client.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/lsp/manager.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/lsp/protocol.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/memory/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/memory/extractor.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/memory/loader.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/memory/store.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/permissions.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/plugin_cache.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/providers.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/runner.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/runtime.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/select.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/session.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/sinks.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/streaming.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tool_policy.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/_diff.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/_shared.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/apply_patch.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/apply_patch_prompt.txt +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/delegate.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/delegate_prompt.txt +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/file_ops.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/lsp.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/memory_tool.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/plan_mode.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/registry.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/search.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/shell.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/skill.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/web.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tools/worktree.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/sanitize.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/screens/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/screens/choice.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/screens/confirm.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/screens/search.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/screens/text_input.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/sinks.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/slash_bridge.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/ui.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/__init__.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/chat.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/completer.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/context_pane.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/header.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/inline_choice.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/loaded_pane.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/status.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/thinking.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/tui/widgets/tools.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru/ui.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/setup.cfg +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_agents_md_coverage.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_apply_patch.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_async_tool_permission.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cache_patch_metrics.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cache_patch_stop_reason.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_catalog.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_chat_scrollable.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_codebase.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_config.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_context.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_context_pane.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_cwd_awareness.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_delegate.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_events_backward_compat.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_events_schema.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_fork_ctx_concurrency.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_format.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_invoke_skill.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_invoked_skills.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_loaded_pane_path.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_lsp.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_lsp_rename.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_main.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_markdown_to_text.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_mcp_health.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_memory.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_memory_tool.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_microcompact.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_permissions.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_plan_mode_refactor.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_plugin_cache.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_plugin_errors.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_plugin_hooks_v2.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_plugins.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_providers.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_ranker.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_reasoning.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_runner_interrupt.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_runner_recovery.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_runtime.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_select.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_skill_disallowed_tools.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_status_breakdown.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_status_cost.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_streaming_sink.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tasklist.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_thread_tool_timeout.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tool_policy.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_truncation_marker.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_app_boot.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_bindings.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_bus_flow.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_chat.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_chat_adversarial.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_completer.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_completer_dynamic.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_copy.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_input_behaviour.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_layer12_recovery.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_layer13_recovery.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_mention_expand.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_modals.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_mode_cycle.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_native_selection.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_permission_flow.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_plan_task_render.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_sidebar_toggle.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_slash_bridge.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_snapshot_smoke.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_thinking_and_boot.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_tui_widgets_visual.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_ui_adapter.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_worktree.py +0 -0
- {aru_code-0.46.0 → aru_code-0.47.0}/tests/test_worktree_session_restore.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.47.0"
|
|
@@ -647,6 +647,27 @@ class AruApp(App):
|
|
|
647
647
|
if text.startswith("/") and self._maybe_run_local_slash(text):
|
|
648
648
|
return
|
|
649
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
|
+
|
|
650
671
|
if self._busy:
|
|
651
672
|
self.query_one(ChatPane).add_system_message(
|
|
652
673
|
"Agent is busy — wait for the current turn to finish."
|
|
@@ -963,6 +984,7 @@ class AruApp(App):
|
|
|
963
984
|
" /clear clear chat pane",
|
|
964
985
|
" /plan toggle plan mode",
|
|
965
986
|
" /quit /exit save session and exit",
|
|
987
|
+
" ! <command> run a shell command (output streams to chat)",
|
|
966
988
|
"",
|
|
967
989
|
"Shortcuts:",
|
|
968
990
|
" Ctrl+Q quit",
|
|
@@ -1128,6 +1150,121 @@ class AruApp(App):
|
|
|
1128
1150
|
# without waiting for the periodic Layer 10 tick.
|
|
1129
1151
|
self._reenable_mouse_tracking()
|
|
1130
1152
|
|
|
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
|
+
|
|
1131
1268
|
# Layer 14 — full set of DEC private modes that ``WindowsDriver
|
|
1132
1269
|
# .start_application_mode`` enables at boot, minus alt-screen
|
|
1133
1270
|
# (``?1049``, not idempotent — would save/restore the display
|
|
@@ -174,6 +174,7 @@ tests/test_tui_mode_cycle.py
|
|
|
174
174
|
tests/test_tui_native_selection.py
|
|
175
175
|
tests/test_tui_permission_flow.py
|
|
176
176
|
tests/test_tui_plan_task_render.py
|
|
177
|
+
tests/test_tui_shell_bang.py
|
|
177
178
|
tests/test_tui_sidebar_toggle.py
|
|
178
179
|
tests/test_tui_slash_bridge.py
|
|
179
180
|
tests/test_tui_snapshot_smoke.py
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Tests for the ``! <command>`` shell escape in the TUI.
|
|
2
|
+
|
|
3
|
+
The TUI mirrors the REPL's ``! cmd`` path: typing ``! echo hi`` runs
|
|
4
|
+
``echo hi`` locally (in the session cwd), streams output into the chat
|
|
5
|
+
pane, and reports the exit code — without invoking the agent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
pytest.importorskip("textual")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.asyncio
|
|
18
|
+
async def test_bang_dispatches_shell_not_agent():
|
|
19
|
+
"""``! cmd`` should run via _dispatch_shell_command, NOT _dispatch_user_turn."""
|
|
20
|
+
from aru.tui.app import AruApp
|
|
21
|
+
from textual.widgets import Input
|
|
22
|
+
|
|
23
|
+
captured: dict = {}
|
|
24
|
+
|
|
25
|
+
class _Probe(AruApp):
|
|
26
|
+
def _dispatch_user_turn(self, text: str) -> None: # type: ignore[override]
|
|
27
|
+
captured["agent_text"] = text
|
|
28
|
+
|
|
29
|
+
def _dispatch_shell_command(self, cmd: str) -> None: # type: ignore[override]
|
|
30
|
+
captured["shell_cmd"] = cmd
|
|
31
|
+
|
|
32
|
+
app = _Probe()
|
|
33
|
+
async with app.run_test() as pilot:
|
|
34
|
+
await pilot.pause()
|
|
35
|
+
inp = app.query_one(Input)
|
|
36
|
+
inp.post_message(Input.Submitted(inp, value="! echo hi"))
|
|
37
|
+
await pilot.pause()
|
|
38
|
+
|
|
39
|
+
assert captured.get("shell_cmd") == "echo hi"
|
|
40
|
+
assert "agent_text" not in captured
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_bang_empty_command_warns():
|
|
45
|
+
"""``! `` alone should show a usage message and not dispatch anything."""
|
|
46
|
+
from aru.tui.app import AruApp
|
|
47
|
+
from aru.tui.widgets.chat import ChatMessageWidget, ChatPane
|
|
48
|
+
from textual.widgets import Input
|
|
49
|
+
|
|
50
|
+
captured: dict = {}
|
|
51
|
+
|
|
52
|
+
class _Probe(AruApp):
|
|
53
|
+
def _dispatch_user_turn(self, text: str) -> None: # type: ignore[override]
|
|
54
|
+
captured["agent_text"] = text
|
|
55
|
+
|
|
56
|
+
def _dispatch_shell_command(self, cmd: str) -> None: # type: ignore[override]
|
|
57
|
+
captured["shell_cmd"] = cmd
|
|
58
|
+
|
|
59
|
+
app = _Probe()
|
|
60
|
+
async with app.run_test() as pilot:
|
|
61
|
+
await pilot.pause()
|
|
62
|
+
inp = app.query_one(Input)
|
|
63
|
+
inp.post_message(Input.Submitted(inp, value="! "))
|
|
64
|
+
await pilot.pause()
|
|
65
|
+
chat = app.query_one(ChatPane)
|
|
66
|
+
msgs = list(chat.query(ChatMessageWidget))
|
|
67
|
+
joined = " ".join(m.buffer for m in msgs)
|
|
68
|
+
|
|
69
|
+
assert "Usage:" in joined
|
|
70
|
+
assert "shell_cmd" not in captured
|
|
71
|
+
assert "agent_text" not in captured
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_bang_busy_blocks_dispatch():
|
|
76
|
+
"""If the app is already busy, ``! cmd`` should refuse to start."""
|
|
77
|
+
from aru.tui.app import AruApp
|
|
78
|
+
from textual.widgets import Input
|
|
79
|
+
|
|
80
|
+
captured: dict = {}
|
|
81
|
+
|
|
82
|
+
class _Probe(AruApp):
|
|
83
|
+
def _dispatch_shell_command(self, cmd: str) -> None: # type: ignore[override]
|
|
84
|
+
captured["shell_cmd"] = cmd
|
|
85
|
+
|
|
86
|
+
app = _Probe()
|
|
87
|
+
async with app.run_test() as pilot:
|
|
88
|
+
await pilot.pause()
|
|
89
|
+
app._busy = True
|
|
90
|
+
inp = app.query_one(Input)
|
|
91
|
+
inp.post_message(Input.Submitted(inp, value="! echo hi"))
|
|
92
|
+
await pilot.pause()
|
|
93
|
+
|
|
94
|
+
assert "shell_cmd" not in captured
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_bang_runs_real_command_and_streams_output():
|
|
99
|
+
"""End-to-end: a real shell command's output reaches the chat pane."""
|
|
100
|
+
from aru.tui.app import AruApp
|
|
101
|
+
from aru.tui.widgets.chat import ChatMessageWidget, ChatPane
|
|
102
|
+
from textual.widgets import Input
|
|
103
|
+
|
|
104
|
+
# Pick a command that works on both Windows and POSIX. ``python -c``
|
|
105
|
+
# avoids shell-specific syntax (echo behaves differently between
|
|
106
|
+
# cmd.exe and bash) and forces a known output line.
|
|
107
|
+
py = sys.executable.replace("\\", "/")
|
|
108
|
+
command = f'{py} -c "print(\'aru-shell-marker\')"'
|
|
109
|
+
|
|
110
|
+
app = AruApp()
|
|
111
|
+
async with app.run_test() as pilot:
|
|
112
|
+
await pilot.pause()
|
|
113
|
+
inp = app.query_one(Input)
|
|
114
|
+
inp.post_message(Input.Submitted(inp, value=f"! {command}"))
|
|
115
|
+
# Wait for the worker to finish — _busy flips back to False once
|
|
116
|
+
# the subprocess exits and the finally block runs.
|
|
117
|
+
for _ in range(200):
|
|
118
|
+
await pilot.pause(0.05)
|
|
119
|
+
if not app._busy:
|
|
120
|
+
break
|
|
121
|
+
chat = app.query_one(ChatPane)
|
|
122
|
+
msgs = list(chat.query(ChatMessageWidget))
|
|
123
|
+
joined = "\n".join(m.buffer for m in msgs)
|
|
124
|
+
|
|
125
|
+
assert "aru-shell-marker" in joined
|
|
126
|
+
assert "[exit 0]" in joined
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_bang_does_not_persist_to_session_history():
|
|
131
|
+
"""Shell runs are local — they must not land in session.history.
|
|
132
|
+
|
|
133
|
+
Otherwise the agent would see ``! ls``-style turns on the next
|
|
134
|
+
prompt and try to reason about them as if the user had said them.
|
|
135
|
+
"""
|
|
136
|
+
from aru.tui.app import AruApp
|
|
137
|
+
from aru.session import Session
|
|
138
|
+
from textual.widgets import Input
|
|
139
|
+
|
|
140
|
+
app = AruApp()
|
|
141
|
+
async with app.run_test() as pilot:
|
|
142
|
+
await pilot.pause()
|
|
143
|
+
app.session = Session(session_id="test-shell-no-history")
|
|
144
|
+
inp = app.query_one(Input)
|
|
145
|
+
inp.post_message(Input.Submitted(inp, value="! echo hi"))
|
|
146
|
+
# Don't even need to wait for the worker — persistence (or lack
|
|
147
|
+
# thereof) is decided synchronously during dispatch.
|
|
148
|
+
await pilot.pause()
|
|
149
|
+
# Stop the worker promptly so the test exits cleanly.
|
|
150
|
+
try:
|
|
151
|
+
for w in list(app.workers):
|
|
152
|
+
w.cancel()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
user_msgs = [m for m in app.session.history if m.get("role") == "user"]
|
|
157
|
+
assert user_msgs == []
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.asyncio
|
|
161
|
+
async def test_bang_failing_command_reports_nonzero_exit():
|
|
162
|
+
"""A command that exits non-zero should still surface an exit-code line."""
|
|
163
|
+
from aru.tui.app import AruApp
|
|
164
|
+
from aru.tui.widgets.chat import ChatMessageWidget, ChatPane
|
|
165
|
+
from textual.widgets import Input
|
|
166
|
+
|
|
167
|
+
py = sys.executable.replace("\\", "/")
|
|
168
|
+
command = f'{py} -c "import sys; sys.exit(7)"'
|
|
169
|
+
|
|
170
|
+
app = AruApp()
|
|
171
|
+
async with app.run_test() as pilot:
|
|
172
|
+
await pilot.pause()
|
|
173
|
+
inp = app.query_one(Input)
|
|
174
|
+
inp.post_message(Input.Submitted(inp, value=f"! {command}"))
|
|
175
|
+
for _ in range(200):
|
|
176
|
+
await pilot.pause(0.05)
|
|
177
|
+
if not app._busy:
|
|
178
|
+
break
|
|
179
|
+
chat = app.query_one(ChatPane)
|
|
180
|
+
msgs = list(chat.query(ChatMessageWidget))
|
|
181
|
+
joined = "\n".join(m.buffer for m in msgs)
|
|
182
|
+
|
|
183
|
+
assert "[exit 7]" in joined
|
aru_code-0.46.0/aru/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.46.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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|