aru-code 0.44.0__tar.gz → 0.46.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.44.0/aru_code.egg-info → aru_code-0.46.0}/PKG-INFO +1 -1
- aru_code-0.46.0/aru/__init__.py +1 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/app.py +202 -27
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/chat.py +256 -0
- {aru_code-0.44.0 → aru_code-0.46.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/SOURCES.txt +2 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/pyproject.toml +1 -1
- aru_code-0.46.0/tests/test_tui_layer12_recovery.py +154 -0
- aru_code-0.46.0/tests/test_tui_layer13_recovery.py +116 -0
- aru_code-0.44.0/aru/__init__.py +0 -1
- {aru_code-0.44.0 → aru_code-0.46.0}/LICENSE +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/README.md +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/agent_factory.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/base.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/catalog.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/agents/planner.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/cache_patch.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/checkpoints.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/cli.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/commands.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/completers.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/config.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/context.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/display.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/events.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/format/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/format/manager.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/format/runner.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/history_blocks.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/client.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/manager.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/lsp/protocol.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/extractor.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/loader.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/memory/store.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/permissions.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugin_cache.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/providers.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/runner.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/runtime.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/select.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/session.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/sinks.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/streaming.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tool_policy.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/_diff.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/_shared.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/apply_patch.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/apply_patch_prompt.txt +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/delegate.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/delegate_prompt.txt +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/file_ops.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/lsp.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/memory_tool.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/plan_mode.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/registry.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/search.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/shell.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/skill.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/web.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tools/worktree.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/sanitize.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/choice.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/confirm.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/search.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/screens/text_input.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/sinks.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/slash_bridge.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/ui.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/__init__.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/completer.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/context_pane.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/header.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/inline_choice.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/loaded_pane.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/status.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/thinking.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/tui/widgets/tools.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru/ui.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/setup.cfg +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_agents_md_coverage.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_apply_patch.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_async_tool_permission.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cache_patch_metrics.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cache_patch_stop_reason.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_catalog.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_chat_scrollable.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_codebase.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_config.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_context.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_context_pane.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_cwd_awareness.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_delegate.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_events_backward_compat.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_events_schema.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_fork_ctx_concurrency.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_format.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_invoke_skill.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_invoked_skills.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_loaded_pane_path.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_lsp.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_lsp_rename.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_main.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_markdown_to_text.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_mcp_health.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_memory.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_memory_tool.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_microcompact.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_permissions.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plan_mode_refactor.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugin_cache.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugin_errors.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugin_hooks_v2.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_plugins.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_providers.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_ranker.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_reasoning.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_runner_interrupt.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_runner_recovery.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_runtime.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_select.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_skill_disallowed_tools.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_status_breakdown.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_status_cost.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_streaming_sink.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tasklist.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_thread_tool_timeout.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tool_policy.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_truncation_marker.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_app_boot.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_bindings.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_bus_flow.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_chat.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_chat_adversarial.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_completer.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_completer_dynamic.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_copy.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_input_behaviour.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_mention_expand.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_modals.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_mode_cycle.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_native_selection.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_permission_flow.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_plan_task_render.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_sidebar_toggle.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_slash_bridge.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_snapshot_smoke.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_thinking_and_boot.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_tui_widgets_visual.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_ui_adapter.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_worktree.py +0 -0
- {aru_code-0.44.0 → aru_code-0.46.0}/tests/test_worktree_session_restore.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.46.0"
|
|
@@ -32,6 +32,7 @@ from __future__ import annotations
|
|
|
32
32
|
|
|
33
33
|
import asyncio
|
|
34
34
|
import sys
|
|
35
|
+
import time
|
|
35
36
|
from typing import Any
|
|
36
37
|
|
|
37
38
|
from textual.app import App, ComposeResult
|
|
@@ -244,6 +245,11 @@ class AruApp(App):
|
|
|
244
245
|
Binding("ctrl+b", "toggle_sidebar", "Sidebar", show=True),
|
|
245
246
|
Binding("ctrl+y", "copy_last", "Copy last", show=True),
|
|
246
247
|
Binding("ctrl+shift+y", "copy_all", "Copy all", show=False),
|
|
248
|
+
# Layer 13 — user-invoked terminal recovery. priority=True so the
|
|
249
|
+
# binding fires before any focused widget can absorb the key, in
|
|
250
|
+
# case Textual ever reclassifies ctrl+r as printable on some
|
|
251
|
+
# platform. See ``action_recover_terminal`` for what it does.
|
|
252
|
+
Binding("ctrl+r", "recover_terminal", "Recover", show=True, priority=True),
|
|
247
253
|
Binding("up", "history_prev", "Prev", show=False, priority=False),
|
|
248
254
|
Binding("down", "history_next", "Next", show=False, priority=False),
|
|
249
255
|
]
|
|
@@ -256,12 +262,24 @@ class AruApp(App):
|
|
|
256
262
|
"skills", "agents", "commands", "mcp", "yolo",
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
# Layer 10 — interval (seconds) between belt-and-suspenders re-emits
|
|
260
|
-
# the mouse-tracking enable sequences. 8s
|
|
261
|
-
#
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
+
# Layer 10 / 12 — interval (seconds) between belt-and-suspenders re-emits
|
|
266
|
+
# of the mouse-tracking enable sequences. Was 8s pre-Layer-12; user
|
|
267
|
+
# report on 2026-04-25 against ``final-fantasy-9/.aru/sessions/b33dfb99``
|
|
268
|
+
# was that the wheel never came back even after the turn ended in YOLO,
|
|
269
|
+
# and 8s was visibly long enough that the user gave up before the next
|
|
270
|
+
# tick. 3s is short enough that a corrupted state self-heals before the
|
|
271
|
+
# next mouse interaction, and the cost is still ~64 bytes per tick (the
|
|
272
|
+
# Layer 12 off-then-on shake — see ``_reenable_mouse_tracking``).
|
|
273
|
+
_MOUSE_REENABLE_INTERVAL: float = 3.0
|
|
274
|
+
|
|
275
|
+
# Layer 12 — minimum interval (seconds) between keypress-triggered
|
|
276
|
+
# mouse-tracking re-arms. Each keystroke is an opportunity to recover
|
|
277
|
+
# — if the user is typing it might be precisely BECAUSE the wheel just
|
|
278
|
+
# stopped working — but we don't want a fast typist to turn every
|
|
279
|
+
# keystroke into four extra terminal writes. 500 ms is below human
|
|
280
|
+
# noticeable retry latency yet caps the keystroke→write amplification
|
|
281
|
+
# at ~2 Hz worst case.
|
|
282
|
+
_KEYPRESS_REARM_DEBOUNCE: float = 0.5
|
|
265
283
|
|
|
266
284
|
def __init__(
|
|
267
285
|
self,
|
|
@@ -288,6 +306,12 @@ class AruApp(App):
|
|
|
288
306
|
# cleared) by on_input_submitted.
|
|
289
307
|
self._pending_paste: str | None = None
|
|
290
308
|
self._pending_paste_lines: int = 0
|
|
309
|
+
# Layer 12 — last time we re-emitted the mouse-tracking enable
|
|
310
|
+
# sequences via the keypress path. Used to debounce per-keystroke
|
|
311
|
+
# re-arming so a fast typist doesn't spam the terminal with re-
|
|
312
|
+
# enables. Initialised to negative infinity so the first keystroke
|
|
313
|
+
# always rearms.
|
|
314
|
+
self._last_mouse_reenable_at: float = float("-inf")
|
|
291
315
|
|
|
292
316
|
# ── Composition ──────────────────────────────────────────────────
|
|
293
317
|
|
|
@@ -549,7 +573,14 @@ class AruApp(App):
|
|
|
549
573
|
suggestion and fires ``Input.Submitted``, which produced the
|
|
550
574
|
"three Enters to run /help" glitch. Tab is the only key that
|
|
551
575
|
accepts the highlighted suggestion.
|
|
576
|
+
|
|
577
|
+
Layer 12 — every keystroke is also a recovery opportunity. The
|
|
578
|
+
Layer 10 periodic tick still runs every ``_MOUSE_REENABLE_INTERVAL``
|
|
579
|
+
but a typing user wants the wheel back NOW, not in three seconds.
|
|
580
|
+
Debounced via ``_KEYPRESS_REARM_DEBOUNCE`` so a fast typist
|
|
581
|
+
doesn't amplify each keystroke into four extra terminal writes.
|
|
552
582
|
"""
|
|
583
|
+
self._maybe_rearm_mouse_on_keypress()
|
|
553
584
|
try:
|
|
554
585
|
completer = self.query_one(SlashCompleter)
|
|
555
586
|
except Exception:
|
|
@@ -1097,34 +1128,138 @@ class AruApp(App):
|
|
|
1097
1128
|
# without waiting for the periodic Layer 10 tick.
|
|
1098
1129
|
self._reenable_mouse_tracking()
|
|
1099
1130
|
|
|
1131
|
+
# Layer 14 — full set of DEC private modes that ``WindowsDriver
|
|
1132
|
+
# .start_application_mode`` enables at boot, minus alt-screen
|
|
1133
|
+
# (``?1049``, not idempotent — would save/restore the display
|
|
1134
|
+
# buffer) and kitty-keyboard (``>1u``, terminal-specific, doesn't
|
|
1135
|
+
# affect wheel). Layer 13 introduced this set as a Ctrl+R-only
|
|
1136
|
+
# heavy shake; user confirmation that Ctrl+R actually recovered
|
|
1137
|
+
# the wheel after Windows display sleep/wake (2026-04-25) is the
|
|
1138
|
+
# signal that the broader set is what works in practice — the
|
|
1139
|
+
# mouse-only shake from Layer 12 was insufficient. Layer 14 promotes
|
|
1140
|
+
# the full set into ``_reenable_mouse_tracking`` so every existing
|
|
1141
|
+
# caller (Layer 9 turn boundary, Layer 10 periodic tick, Layer 12
|
|
1142
|
+
# broken keypress) gets the proven recovery automatically.
|
|
1143
|
+
_FULL_MODE_DISABLE_SEQS: tuple[str, ...] = (
|
|
1144
|
+
"\x1b[?1000l", # mouse VT200
|
|
1145
|
+
"\x1b[?1003l", # any-event mouse
|
|
1146
|
+
"\x1b[?1015l", # VT200 highlight mouse
|
|
1147
|
+
"\x1b[?1006l", # SGR ext mode mouse
|
|
1148
|
+
"\x1b[?1004l", # focus events
|
|
1149
|
+
"\x1b[?2004l", # bracketed paste
|
|
1150
|
+
)
|
|
1151
|
+
_FULL_MODE_ENABLE_SEQS: tuple[str, ...] = (
|
|
1152
|
+
"\x1b[?1000h",
|
|
1153
|
+
"\x1b[?1003h",
|
|
1154
|
+
"\x1b[?1015h",
|
|
1155
|
+
"\x1b[?1006h",
|
|
1156
|
+
"\x1b[?1004h",
|
|
1157
|
+
"\x1b[?2004h",
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1100
1160
|
def _reenable_mouse_tracking(self) -> None:
|
|
1101
|
-
"""Re-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
documented
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1161
|
+
"""Re-arm terminal modes via console-mode re-assert + full-mode shake.
|
|
1162
|
+
|
|
1163
|
+
Single recovery primitive used by every layer: turn boundary
|
|
1164
|
+
(Layer 9), periodic tick (Layer 10), keypress trigger (Layer 12,
|
|
1165
|
+
broken — see chat.py post-mortem), and ``Ctrl+R`` action (Layer
|
|
1166
|
+
13, which adds a refresh + chat message on top). The method
|
|
1167
|
+
keeps its name (``_reenable_mouse_tracking``) for git-blame
|
|
1168
|
+
continuity even though it now re-arms more than just mouse —
|
|
1169
|
+
what it does is documented here, and the post-mortem in
|
|
1170
|
+
chat.py traces the evolution from Layer 12 through Layer 14.
|
|
1171
|
+
|
|
1172
|
+
Two failure modes the recovery handles:
|
|
1173
|
+
|
|
1174
|
+
1. **``ENABLE_VIRTUAL_TERMINAL_INPUT`` cleared on stdin (Windows).**
|
|
1175
|
+
``enable_application_mode`` (textual win32.py:179) sets this
|
|
1176
|
+
flag at startup, but a display sleep / wake or other Windows
|
|
1177
|
+
console state transition can clear it. While cleared,
|
|
1178
|
+
ConPTY stops translating mouse / focus events into VT
|
|
1179
|
+
sequences and *no* stdout escape we write can recover wheel
|
|
1180
|
+
input. Re-asserting the flag additively (``current | flag``)
|
|
1181
|
+
preserves any other input flags while ensuring VT input
|
|
1182
|
+
translation is back on.
|
|
1183
|
+
|
|
1184
|
+
2. **DEC private-mode state lost on the terminal side.** Layer
|
|
1185
|
+
12 originally addressed this for mouse-only via an off-then-on
|
|
1186
|
+
shake (``?1000l → ?1000h``) to defeat ConPTY's enable-cache.
|
|
1187
|
+
Layer 14 widens the shake to the full set ``WindowsDriver
|
|
1188
|
+
.start_application_mode`` enables: mouse (4 modes) + focus
|
|
1189
|
+
events (``?1004``) + bracketed paste (``?2004``). 12 escapes
|
|
1190
|
+
total off-then-on, ~108 bytes, one flush. Excluded:
|
|
1191
|
+
alt-screen (not idempotent) and kitty-keyboard (terminal-
|
|
1192
|
+
specific, doesn't affect wheel). The user report on
|
|
1193
|
+
2026-04-25 confirmed the mouse-only shake didn't recover
|
|
1194
|
+
the wheel after display wake but the full shake (via Ctrl+R)
|
|
1195
|
+
did — Layer 14 promotes that proven recovery into the auto
|
|
1196
|
+
path.
|
|
1197
|
+
|
|
1198
|
+
Cost per call: ~108 bytes + one ``GetConsoleMode`` +
|
|
1199
|
+
``SetConsoleMode`` syscall pair on Windows. At the 3s tick
|
|
1200
|
+
rate that is ~36 B/s plus microseconds — negligible.
|
|
1201
|
+
|
|
1202
|
+
Wrapped in ``try/except`` everywhere because the driver may be
|
|
1203
|
+
``None`` in headless / test mode and the win32 import may fail
|
|
1204
|
+
on non-Windows; we'd rather no-op silently than crash.
|
|
1120
1205
|
"""
|
|
1206
|
+
if sys.platform == "win32":
|
|
1207
|
+
try:
|
|
1208
|
+
from textual.drivers.win32 import (
|
|
1209
|
+
ENABLE_VIRTUAL_TERMINAL_INPUT,
|
|
1210
|
+
get_console_mode,
|
|
1211
|
+
set_console_mode,
|
|
1212
|
+
)
|
|
1213
|
+
current = get_console_mode(sys.__stdin__)
|
|
1214
|
+
set_console_mode(
|
|
1215
|
+
sys.__stdin__, current | ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
1216
|
+
)
|
|
1217
|
+
except Exception:
|
|
1218
|
+
pass
|
|
1219
|
+
|
|
1121
1220
|
try:
|
|
1122
1221
|
driver = self._driver
|
|
1123
|
-
if driver is
|
|
1124
|
-
|
|
1222
|
+
if driver is None:
|
|
1223
|
+
return
|
|
1224
|
+
for seq in self._FULL_MODE_DISABLE_SEQS:
|
|
1225
|
+
try:
|
|
1226
|
+
driver.write(seq)
|
|
1227
|
+
except Exception:
|
|
1228
|
+
pass
|
|
1229
|
+
for seq in self._FULL_MODE_ENABLE_SEQS:
|
|
1230
|
+
try:
|
|
1231
|
+
driver.write(seq)
|
|
1232
|
+
except Exception:
|
|
1233
|
+
pass
|
|
1234
|
+
try:
|
|
1235
|
+
driver.flush()
|
|
1236
|
+
except Exception:
|
|
1237
|
+
pass
|
|
1125
1238
|
except Exception:
|
|
1126
1239
|
pass
|
|
1127
1240
|
|
|
1241
|
+
def _maybe_rearm_mouse_on_keypress(self) -> None:
|
|
1242
|
+
"""Layer 12 — re-arm mouse tracking on each keystroke (debounced).
|
|
1243
|
+
|
|
1244
|
+
Trigger fires from ``on_key`` so any user keypress is treated as a
|
|
1245
|
+
recovery opportunity. A typing user is the strongest signal we
|
|
1246
|
+
have that the wheel just stopped working — they reached for the
|
|
1247
|
+
keyboard because the mouse stopped responding, or they're about
|
|
1248
|
+
to scroll back with PgUp and want it ready. Either way, paying
|
|
1249
|
+
~64 bytes per keypress (capped at 2 Hz by ``_KEYPRESS_REARM_DEBOUNCE``)
|
|
1250
|
+
is a trivial cost for sub-second recovery latency.
|
|
1251
|
+
|
|
1252
|
+
The debounce intentionally uses ``time.monotonic`` rather than the
|
|
1253
|
+
Textual scheduler so it survives across the async ``on_key``
|
|
1254
|
+
boundary without an extra task. ``-inf`` initial value guarantees
|
|
1255
|
+
the first keystroke always rearms.
|
|
1256
|
+
"""
|
|
1257
|
+
now = time.monotonic()
|
|
1258
|
+
if now - self._last_mouse_reenable_at < self._KEYPRESS_REARM_DEBOUNCE:
|
|
1259
|
+
return
|
|
1260
|
+
self._last_mouse_reenable_at = now
|
|
1261
|
+
self._reenable_mouse_tracking()
|
|
1262
|
+
|
|
1128
1263
|
def _self_heal_terminal_state(self) -> None:
|
|
1129
1264
|
"""Periodic recovery of mouse tracking and input focus (Layers 10 + 11).
|
|
1130
1265
|
|
|
@@ -1345,6 +1480,46 @@ class AruApp(App):
|
|
|
1345
1480
|
except Exception:
|
|
1346
1481
|
pass
|
|
1347
1482
|
|
|
1483
|
+
def action_recover_terminal(self) -> None:
|
|
1484
|
+
"""Layer 13 — user-invoked terminal-state recovery (Ctrl+R).
|
|
1485
|
+
|
|
1486
|
+
Delegates the recovery sequence (Windows console-mode re-assert
|
|
1487
|
+
+ full DEC private-mode shake + flush) to
|
|
1488
|
+
``_reenable_mouse_tracking`` — that method now does the strong
|
|
1489
|
+
shake for every layer (Layer 14 promotion), so Ctrl+R, the 3s
|
|
1490
|
+
tick, and the turn-boundary call all run identical recovery
|
|
1491
|
+
bytes. This action adds two extras unique to the manual path:
|
|
1492
|
+
|
|
1493
|
+
* ``self.refresh()`` to force a compositor redraw — the
|
|
1494
|
+
autonomous paths don't need this because the next paint
|
|
1495
|
+
cycle handles it; Ctrl+R is interactive and the user wants
|
|
1496
|
+
immediate visible confirmation.
|
|
1497
|
+
* **Visible chat message** so the user sees the recovery did
|
|
1498
|
+
execute. The user explicitly noted that silent recovery is
|
|
1499
|
+
indistinguishable from no recovery, so we surface it on the
|
|
1500
|
+
manual path. Periodic / turn-boundary callers stay silent
|
|
1501
|
+
to avoid spamming the chat.
|
|
1502
|
+
|
|
1503
|
+
Bound to ``Ctrl+R`` with ``priority=True`` so the binding fires
|
|
1504
|
+
regardless of focused widget. Bindings dispatch via Textual's
|
|
1505
|
+
binding system, not through ``_on_key``, so this path is immune
|
|
1506
|
+
to the ``Input._on_key → event.stop()`` problem that breaks
|
|
1507
|
+
Layer 12's keypress trigger.
|
|
1508
|
+
"""
|
|
1509
|
+
self._reenable_mouse_tracking()
|
|
1510
|
+
|
|
1511
|
+
try:
|
|
1512
|
+
self.refresh()
|
|
1513
|
+
except Exception:
|
|
1514
|
+
pass
|
|
1515
|
+
|
|
1516
|
+
try:
|
|
1517
|
+
self.query_one(ChatPane).add_system_message(
|
|
1518
|
+
"[Ctrl+R] Terminal modes re-armed (mouse / focus / paste)"
|
|
1519
|
+
)
|
|
1520
|
+
except Exception:
|
|
1521
|
+
pass
|
|
1522
|
+
|
|
1348
1523
|
def action_toggle_sidebar(self) -> None:
|
|
1349
1524
|
"""Hide / show the right sidebar to give the chat full width."""
|
|
1350
1525
|
try:
|
|
@@ -345,6 +345,262 @@ virtualisation experiments) would close some of these by structure,
|
|
|
345
345
|
but at the cost of every other property the chat currently has
|
|
346
346
|
(selection, copy, mid-stream insertion of arbitrary Rich panels, plan
|
|
347
347
|
mounts). Layered defences are cheap and additive; the rewrite is not.
|
|
348
|
+
|
|
349
|
+
----
|
|
350
|
+
|
|
351
|
+
Post-mortem — "self-heal didn't recover the wheel" (2026-04-25,
|
|
352
|
+
``fix/scroll-analysis2``)
|
|
353
|
+
---------------------------------------------------------------------
|
|
354
|
+
**Symptom:** wheel-dead reproduced again post-Layer-11, this time
|
|
355
|
+
against ``final-fantasy-9/.aru/sessions/b33dfb99``. After a YOLO turn
|
|
356
|
+
finished, mouse wheel stopped working on every scrollable surface
|
|
357
|
+
simultaneously — the canonical Layer 7/9/10 fingerprint. User report:
|
|
358
|
+
*"não vi nenhum self healing seu funcionar até agora"* — Layers 9
|
|
359
|
+
(turn-boundary call) and 10 (8 s periodic tick) ran, the four
|
|
360
|
+
``?1000h``/``?1003h``/``?1015h``/``?1006h`` enables were emitted, and
|
|
361
|
+
yet the wheel never came back.
|
|
362
|
+
|
|
363
|
+
A byte scan of the persisted session turned up zero ``\\x1b`` bytes —
|
|
364
|
+
same shape as the Layer 9 incident. The leak is from a non-persisted
|
|
365
|
+
path (transient tool output, panel content, ConPTY-side state drift)
|
|
366
|
+
and we accept we won't trace its emitter. The real question Layer 12
|
|
367
|
+
answers is: **why didn't the recovery sequences work even when they
|
|
368
|
+
fired?**
|
|
369
|
+
|
|
370
|
+
**Two root causes the previous re-enable couldn't address:**
|
|
371
|
+
|
|
372
|
+
1. **ConPTY enable-cache.** Windows ConPTY tracks DEC private-mode
|
|
373
|
+
state on its side. When its cache says ``?1000`` is already ``h``
|
|
374
|
+
it can suppress the write to the underlying terminal — even when
|
|
375
|
+
the terminal itself lost the state. Sending ``?1000h`` while the
|
|
376
|
+
cache thinks it's already on is therefore a no-op against a real
|
|
377
|
+
leak. Our Layer-9/10 emit was exactly this no-op.
|
|
378
|
+
|
|
379
|
+
2. **Driver-side enable gate.** ``WindowsDriver._enable_mouse_support``
|
|
380
|
+
in textual 8.2.4 (``windows_driver.py:55``) opens with
|
|
381
|
+
``if not self._mouse: return``. ``_mouse`` is True by default and
|
|
382
|
+
shouldn't flip in normal use, but our recovery path was a single
|
|
383
|
+
``driver._enable_mouse_support()`` call — if any future Textual
|
|
384
|
+
refactor decides to flip the gate during pause / alt-screen toggle
|
|
385
|
+
/ shutdown, every layer 9 + 10 call silently no-ops and we'd never
|
|
386
|
+
see the failure.
|
|
387
|
+
|
|
388
|
+
**Layer 12 — three coordinated changes** (in ``aru/tui/app.py``):
|
|
389
|
+
|
|
390
|
+
a. **Off-then-on shake instead of enable-only.**
|
|
391
|
+
``_reenable_mouse_tracking`` now emits the four ``?...l`` (off)
|
|
392
|
+
sequences first, then the four ``?...h`` (on) sequences, then
|
|
393
|
+
flushes. The forced state transition defeats ConPTY's enable-cache —
|
|
394
|
+
the cache sees ``l→h`` and propagates the write regardless of what
|
|
395
|
+
it thinks the prior state was. Eight short writes (~64 bytes)
|
|
396
|
+
bufferised into one terminal emit by Textual's ``WriterThread``.
|
|
397
|
+
Idempotent: a healthy terminal lands in the same final state as
|
|
398
|
+
the old enable-only path, with a microscopic transition gap that
|
|
399
|
+
doesn't drop wheel events in practice.
|
|
400
|
+
|
|
401
|
+
b. **Bypass the driver's enable gate.** Recovery now calls
|
|
402
|
+
``driver.write(...)`` for each sequence directly instead of
|
|
403
|
+
``driver._enable_mouse_support()``. The four ``...l`` and four
|
|
404
|
+
``...h`` strings are kept as class-level tuples
|
|
405
|
+
(``_MOUSE_DISABLE_SEQS`` / ``_MOUSE_ENABLE_SEQS``) so any future
|
|
406
|
+
call site (Click handler, focus event) reuses the exact same set
|
|
407
|
+
without drift, and the path is robust against Textual API changes.
|
|
408
|
+
|
|
409
|
+
c. **Keypress trigger + tighter periodic tick.**
|
|
410
|
+
``_MOUSE_REENABLE_INTERVAL`` drops from 8 s to 3 s so worst-case
|
|
411
|
+
self-heal latency is sub-poll-cycle. ``on_key`` calls
|
|
412
|
+
``_maybe_rearm_mouse_on_keypress`` which fires
|
|
413
|
+
``_reenable_mouse_tracking`` on every keystroke debounced to 2 Hz
|
|
414
|
+
(``_KEYPRESS_REARM_DEBOUNCE = 0.5 s``). A typing user is the
|
|
415
|
+
strongest signal we have that they noticed the wheel just stopped
|
|
416
|
+
responding — recovery now happens on the very next keystroke, not
|
|
417
|
+
on the next tick.
|
|
418
|
+
|
|
419
|
+
Layer 12 differs from Layer 10 in the failure-mode it covers, not in
|
|
420
|
+
its shape. Layer 10 said "the bug will keep finding new emitters; let's
|
|
421
|
+
recover on a clock". Layer 12 says "even when we recover on the clock,
|
|
422
|
+
the recovery sequence itself can be ineffective; let's make the
|
|
423
|
+
recovery actually do something". The two stack: tick still runs,
|
|
424
|
+
keypress trigger gives it sub-second latency, off-on shake makes the
|
|
425
|
+
sequence the tick emits actually take effect.
|
|
426
|
+
|
|
427
|
+
----
|
|
428
|
+
|
|
429
|
+
Post-mortem — "no self-heal of Layers 9/10/12 actually recovers"
|
|
430
|
+
(2026-04-25, ``fix/scroll-analysis3``)
|
|
431
|
+
---------------------------------------------------------------------
|
|
432
|
+
**Symptom:** user reported on a Windows display-sleep/wake scenario
|
|
433
|
+
that the wheel dies and *no* existing self-heal recovers it. They
|
|
434
|
+
explicitly observed: "não vi nenhum self healing seu funcionar até
|
|
435
|
+
agora", and confirmed that even sending a message (which fires the
|
|
436
|
+
Layer 9 turn-boundary call) does not bring the wheel back.
|
|
437
|
+
|
|
438
|
+
**Two distinct root causes (one structural, one effectiveness):**
|
|
439
|
+
|
|
440
|
+
1. **Layer 12's keypress trigger is dead during normal typing.**
|
|
441
|
+
``Input._on_key`` in textual 8.x calls ``event.stop()`` for any
|
|
442
|
+
``event.is_printable`` key (``textual/widgets/_input.py:736-737``).
|
|
443
|
+
The event is consumed at the widget level and never bubbles to
|
|
444
|
+
``App.on_key``, so ``_maybe_rearm_mouse_on_keypress`` is never
|
|
445
|
+
invoked while the user types. The Layer 12 tests
|
|
446
|
+
(``tests/test_tui_layer12_recovery.py:109-116``) call the method
|
|
447
|
+
directly and never exercise the real path, so the bug shipped
|
|
448
|
+
silently. Only non-printable keys (Ctrl+combinations, arrows, Esc)
|
|
449
|
+
reach ``App.on_key``, and those also bypass it via the BINDINGS
|
|
450
|
+
system — which dispatches actions directly, again skipping
|
|
451
|
+
``on_key``. In short: **Layer 12 keypress recovery never fires in
|
|
452
|
+
any production scenario**.
|
|
453
|
+
|
|
454
|
+
2. **The off-then-on shake of mouse-only is not sufficient for the
|
|
455
|
+
user's failure mode.** Layer 9 (turn boundary) and Layer 10 (3s
|
|
456
|
+
tick) both fire ``_reenable_mouse_tracking``, which re-emits four
|
|
457
|
+
mouse DEC private modes. But ``WindowsDriver.start_application_mode``
|
|
458
|
+
enables a wider set at boot: mouse + focus-events (``?1004h``) +
|
|
459
|
+
bracketed paste (``?2004h``) + kitty-keyboard. If the display
|
|
460
|
+
sleep/wake corrupts more than just mouse — e.g. the entire VT
|
|
461
|
+
private-mode block on Windows Terminal's side — re-emitting only
|
|
462
|
+
mouse leaves the rest dead and the wheel doesn't come back. There
|
|
463
|
+
is also a possibility that ``ENABLE_VIRTUAL_TERMINAL_INPUT`` on
|
|
464
|
+
stdin gets cleared on wake, in which case **no** stdout escape we
|
|
465
|
+
write can recover wheel input regardless of what we send, because
|
|
466
|
+
ConPTY stops translating mouse events into VT on the input side.
|
|
467
|
+
|
|
468
|
+
**Layer 13 — user-invoked guaranteed-fire recovery (Ctrl+R).**
|
|
469
|
+
``AruApp.action_recover_terminal`` (``app.py``) does four things in
|
|
470
|
+
order:
|
|
471
|
+
|
|
472
|
+
a. **Re-asserts ``ENABLE_VIRTUAL_TERMINAL_INPUT`` on stdin (Windows
|
|
473
|
+
only)** via ``set_console_mode(stdin, current | flag)``. Aditive so
|
|
474
|
+
other input flags survive. Closes the input-side hypothesis from
|
|
475
|
+
root cause 2.
|
|
476
|
+
|
|
477
|
+
b. **Off-then-on shakes the FULL DEC private mode set** —
|
|
478
|
+
``_FULL_MODE_DISABLE_SEQS`` / ``_FULL_MODE_ENABLE_SEQS`` cover
|
|
479
|
+
mouse + focus-events + bracketed-paste. Excludes alt-screen
|
|
480
|
+
(``?1049``, not idempotent — would save/restore the display) and
|
|
481
|
+
kitty-keyboard (terminal-specific, doesn't affect wheel). 12
|
|
482
|
+
escapes total, ~108 bytes, one flush. Broader than Layer 12's
|
|
483
|
+
four-mode shake, so it catches whatever the wake corrupted even
|
|
484
|
+
if we don't know exactly what dropped.
|
|
485
|
+
|
|
486
|
+
c. **``self.refresh()``** to force a compositor redraw. Covers the
|
|
487
|
+
case where Textual's view of the screen drifted from terminal
|
|
488
|
+
reality.
|
|
489
|
+
|
|
490
|
+
d. **Visible system message** in the chat
|
|
491
|
+
(``[Ctrl+R] Terminal modes re-armed...``). The user explicitly
|
|
492
|
+
said silent recovery is indistinguishable from no recovery, so
|
|
493
|
+
we surface that the action did fire — separating "Aru's recovery
|
|
494
|
+
ran but the terminal still won't honor it" from "Aru never
|
|
495
|
+
actually tried".
|
|
496
|
+
|
|
497
|
+
Bound to ``Ctrl+R`` with ``priority=True``. Bindings dispatch through
|
|
498
|
+
Textual's binding system *before* the focused widget's ``_on_key``,
|
|
499
|
+
so this path is immune to the ``Input._on_key → event.stop()`` issue
|
|
500
|
+
that broke Layer 12. The user has a guaranteed-fire keystroke
|
|
501
|
+
regardless of focus state.
|
|
502
|
+
|
|
503
|
+
**What Layer 13 does NOT fix:**
|
|
504
|
+
|
|
505
|
+
* It does not trace the emitter — same as Layer 7/9/10/12, the cause
|
|
506
|
+
of the disable is still outside our reach (likely a ConPTY / Windows
|
|
507
|
+
Terminal interaction during display power transitions).
|
|
508
|
+
* It is not automatic — requires a user keystroke. Layers 9, 10, and
|
|
509
|
+
the (broken) 12 attempted automatic recovery; Layer 13 is the
|
|
510
|
+
manual fallback while we figure out which automatic trigger to
|
|
511
|
+
add. Candidates for a future Layer 14 if the manual shake proves
|
|
512
|
+
effective: hook ``AppFocus`` events, detect long gaps in the
|
|
513
|
+
periodic tick (wake-from-sleep signature), or move the keystroke
|
|
514
|
+
rearm from ``on_key`` to ``on_input_changed`` so it actually fires
|
|
515
|
+
during typing.
|
|
516
|
+
|
|
517
|
+
**Why this is Layer 13 and not a rewrite of Layers 9/10/12:** the
|
|
518
|
+
two failure modes (printable-key absorption and mouse-only-shake
|
|
519
|
+
insufficient) point at distinct fixes; folding them into the existing
|
|
520
|
+
periodic / turn-boundary callers would make every tick emit 12
|
|
521
|
+
escapes plus a Windows console mode call (currently 8 + nothing),
|
|
522
|
+
which is wasteful when the simple shake is enough — and would still
|
|
523
|
+
not fix the printable-key absorption bug. Keeping the strong shake
|
|
524
|
+
in a user-invoked path means:
|
|
525
|
+
|
|
526
|
+
* The cheap path (Layer 9/10) keeps running every 3s with mouse-only.
|
|
527
|
+
* The full path (Layer 13) runs only when the user signals "I
|
|
528
|
+
noticed the wheel is dead, please fix it".
|
|
529
|
+
* If the cheap path is actually sufficient most of the time, we
|
|
530
|
+
don't pay the heavier cost on every tick.
|
|
531
|
+
|
|
532
|
+
If, after Layer 13 ships, the user reports that ``Ctrl+R`` reliably
|
|
533
|
+
recovers but the automatic paths still don't, that proves the strong
|
|
534
|
+
shake is what works and Layers 9/10/12 should be upgraded — that's
|
|
535
|
+
Layer 14's signal. If even ``Ctrl+R`` doesn't recover, the hypothesis
|
|
536
|
+
shifts to "VT escapes are not enough, need ``SetConsoleMode`` direct
|
|
537
|
+
or alt-screen toggle", which is Layer 14 in the other direction.
|
|
538
|
+
|
|
539
|
+
----
|
|
540
|
+
|
|
541
|
+
Post-mortem — "Ctrl+R recovered the wheel; promote the shake"
|
|
542
|
+
(2026-04-25, ``fix/scroll-analysis3`` continued)
|
|
543
|
+
---------------------------------------------------------------------
|
|
544
|
+
**Signal:** the user reported, on the same day Layer 13 shipped, that
|
|
545
|
+
the wheel went dead during a session and pressing ``Ctrl+R`` brought
|
|
546
|
+
it back. That is the exact branch the Layer 13 post-mortem set up:
|
|
547
|
+
"if Ctrl+R reliably recovers but the automatic paths still don't,
|
|
548
|
+
that proves the strong shake is what works and Layers 9/10/12 should
|
|
549
|
+
be upgraded".
|
|
550
|
+
|
|
551
|
+
**Layer 14 — promote the strong shake into ``_reenable_mouse_tracking``.**
|
|
552
|
+
Single change: the body of ``AruApp._reenable_mouse_tracking`` now does
|
|
553
|
+
what Layer 13's ``action_recover_terminal`` did inline — re-assert
|
|
554
|
+
``ENABLE_VIRTUAL_TERMINAL_INPUT`` on stdin (Windows) plus off-then-on
|
|
555
|
+
shake of the full DEC private-mode set (mouse + focus-events +
|
|
556
|
+
bracketed-paste). Every existing caller benefits without further
|
|
557
|
+
changes:
|
|
558
|
+
|
|
559
|
+
* **Layer 9** (``_run_turn`` finally clause) — every turn boundary
|
|
560
|
+
now runs the proven recovery, so a wheel that died mid-turn is back
|
|
561
|
+
before the user reaches for it.
|
|
562
|
+
* **Layer 10** (3s periodic tick via ``_self_heal_terminal_state``)
|
|
563
|
+
— autonomous recovery within 3 seconds of the wheel dying, with
|
|
564
|
+
the shake that actually works.
|
|
565
|
+
* **Layer 12** (per-keystroke trigger via ``_maybe_rearm_mouse_on_keypress``)
|
|
566
|
+
— still broken in production because ``Input._on_key`` consumes
|
|
567
|
+
printable keys before ``App.on_key`` sees them, so the trigger
|
|
568
|
+
never actually fires. Layer 14 doesn't fix this; the rearm primitive
|
|
569
|
+
is still pinned by tests for the day a future Layer wires it to
|
|
570
|
+
``on_input_changed`` (which uses Message bubbling, not key events).
|
|
571
|
+
|
|
572
|
+
``action_recover_terminal`` (Ctrl+R) now delegates the shake to
|
|
573
|
+
``_reenable_mouse_tracking`` and adds two extras unique to the manual
|
|
574
|
+
path: ``self.refresh()`` for immediate visible repaint and the
|
|
575
|
+
``[Ctrl+R] Terminal modes re-armed`` chat message. Periodic / turn-
|
|
576
|
+
boundary callers stay silent — running the strong shake without
|
|
577
|
+
spamming the chat or forcing a redraw every 3s.
|
|
578
|
+
|
|
579
|
+
**Cost of the upgrade:** every periodic tick emits 12 escapes (~108
|
|
580
|
+
bytes) + one ``GetConsoleMode``/``SetConsoleMode`` syscall pair on
|
|
581
|
+
Windows instead of 8 escapes (~64 bytes) + nothing. At 3s cadence
|
|
582
|
+
that is ~36 B/s plus microseconds of syscall — negligible.
|
|
583
|
+
|
|
584
|
+
**Risk acknowledged but not seen:** the off→on transition on
|
|
585
|
+
``?1004`` (focus events) could in theory cause Windows Terminal to
|
|
586
|
+
emit a spurious focus event during the gap. Idempotent in practice
|
|
587
|
+
on healthy terminals (``?1004l → ?1004h`` should leave focus state
|
|
588
|
+
unchanged), but if a future signal points at "focus events firing
|
|
589
|
+
unexpectedly every 3s", the fix is to keep ``?1004`` and ``?2004`` in
|
|
590
|
+
the Ctrl+R-only path and revert the periodic shake to mouse-only.
|
|
591
|
+
|
|
592
|
+
**What Layer 14 does NOT change:**
|
|
593
|
+
|
|
594
|
+
* The Layer 12 keystroke trigger remains broken — ``Input._on_key``
|
|
595
|
+
still absorbs printable keys, so typing in the prompt never
|
|
596
|
+
triggers recovery. Fix candidate for a future layer: hook
|
|
597
|
+
``on_input_changed`` instead of ``on_key`` (uses Message bubbling
|
|
598
|
+
which isn't blocked by widget consumption).
|
|
599
|
+
* The underlying emitter that disables terminal mouse tracking is
|
|
600
|
+
still unidentified — same as every layer back to 7.
|
|
601
|
+
* No new automatic trigger beyond the existing 3s tick + turn
|
|
602
|
+
boundary. Hooking ``AppFocus`` for wake-from-display-blank
|
|
603
|
+
detection is a follow-up if Layer 14 still misses cases.
|
|
348
604
|
"""
|
|
349
605
|
|
|
350
606
|
from __future__ import annotations
|
|
@@ -166,6 +166,8 @@ tests/test_tui_completer.py
|
|
|
166
166
|
tests/test_tui_completer_dynamic.py
|
|
167
167
|
tests/test_tui_copy.py
|
|
168
168
|
tests/test_tui_input_behaviour.py
|
|
169
|
+
tests/test_tui_layer12_recovery.py
|
|
170
|
+
tests/test_tui_layer13_recovery.py
|
|
169
171
|
tests/test_tui_mention_expand.py
|
|
170
172
|
tests/test_tui_modals.py
|
|
171
173
|
tests/test_tui_mode_cycle.py
|