railmux 0.4.0.dev16__tar.gz → 0.4.0.dev18__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.
- {railmux-0.4.0.dev16/src/railmux.egg-info → railmux-0.4.0.dev18}/PKG-INFO +1 -1
- railmux-0.4.0.dev18/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/cli.py +16 -2
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/fast_display_history.py +11 -9
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/tmux_binding_manager.py +5 -2
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/tmux_server.py +50 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/app.py +208 -41
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_cli.py +67 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_fast_full_window.py +43 -11
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_fullscreen_binding.py +206 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_terminal_size.py +12 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tmux_binding_manager.py +25 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tmux_server.py +63 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tmux_status.py +57 -8
- railmux-0.4.0.dev16/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/LICENSE +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/README.md +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/pyproject.toml +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/setup.cfg +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev16 → railmux-0.4.0.dev18}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev18"
|
|
@@ -593,14 +593,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
593
593
|
if Path(sys.argv[0]).name == "__main__.py"
|
|
594
594
|
else [sys.argv[0]]
|
|
595
595
|
)
|
|
596
|
-
|
|
596
|
+
client_env = tmux_server.exec_environment()
|
|
597
597
|
dedicated_session_id = (
|
|
598
598
|
tmux_server.target_session_id(dedicated_target, "railmux")
|
|
599
599
|
if dedicated_target is not None else None
|
|
600
600
|
)
|
|
601
|
+
if dedicated_target is not None and dedicated_session_id is None:
|
|
602
|
+
from railmux.provider_paths import (
|
|
603
|
+
running_in_managed_windows_wrapper,
|
|
604
|
+
)
|
|
605
|
+
if running_in_managed_windows_wrapper():
|
|
606
|
+
dedicated_session_id = (
|
|
607
|
+
tmux_server.ensure_detached_launcher_session(
|
|
608
|
+
dedicated_target,
|
|
609
|
+
launch_prefix,
|
|
610
|
+
raw_args,
|
|
611
|
+
env=client_env,
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
|
|
601
615
|
return _run_tmux_client_with_watchdog(
|
|
602
616
|
cmd,
|
|
603
|
-
|
|
617
|
+
client_env,
|
|
604
618
|
expected_target=dedicated_target,
|
|
605
619
|
expected_session_id=dedicated_session_id,
|
|
606
620
|
)
|
|
@@ -287,15 +287,17 @@ class LocalHistoryView:
|
|
|
287
287
|
# intermediate output above the current screen.
|
|
288
288
|
lines = incoming.lines
|
|
289
289
|
else:
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
# Never splice two captures whose timelines cannot be aligned.
|
|
291
|
+
# Retaining old history and replacing only the live viewport
|
|
292
|
+
# looked conservative, but it silently omitted every row
|
|
293
|
+
# produced between those two pieces. That false seam appeared
|
|
294
|
+
# as discontinuous SSH history until returning to the bottom
|
|
295
|
+
# forced a fresh deep capture. Keep the newest internally
|
|
296
|
+
# contiguous hot capture instead; the first wheel-up requests
|
|
297
|
+
# a cumulative deep page and can recover older rows safely.
|
|
298
|
+
# An already-frozen viewport owns its immutable snapshot and
|
|
299
|
+
# therefore remains undisturbed by this cache replacement.
|
|
300
|
+
lines = incoming.lines
|
|
299
301
|
else:
|
|
300
302
|
start = min(0, delta)
|
|
301
303
|
incoming_end = delta + len(incoming.lines)
|
|
@@ -540,7 +540,6 @@ class SharedTmuxBindingManager:
|
|
|
540
540
|
)
|
|
541
541
|
break
|
|
542
542
|
return False
|
|
543
|
-
self._registered = True
|
|
544
543
|
self._prefix_tab_managed = state["prefix_tab_managed"]
|
|
545
544
|
self._right_click_managed = (
|
|
546
545
|
state["right_click_managed"])
|
|
@@ -552,6 +551,10 @@ class SharedTmuxBindingManager:
|
|
|
552
551
|
state["status_click_managed"])
|
|
553
552
|
self._termux_tap_managed = (
|
|
554
553
|
state["termux_tap_managed"])
|
|
554
|
+
# Publish registration only after every capability is
|
|
555
|
+
# final so concurrent readers cannot observe a torn
|
|
556
|
+
# lease during managed-Windows background setup.
|
|
557
|
+
self._registered = True
|
|
555
558
|
return True
|
|
556
559
|
|
|
557
560
|
backup = tmux_ctl.prepare_root_function_bindings()
|
|
@@ -701,13 +704,13 @@ class SharedTmuxBindingManager:
|
|
|
701
704
|
state["termux_tap_backup"], token=token)
|
|
702
705
|
self._remove_state()
|
|
703
706
|
return False
|
|
704
|
-
self._registered = True
|
|
705
707
|
self._prefix_tab_managed = state["prefix_tab_managed"]
|
|
706
708
|
self._right_click_managed = state["right_click_managed"]
|
|
707
709
|
self._selection_hook_managed = state["selection_hook_managed"]
|
|
708
710
|
self._selection_hook_index = state["selection_hook_index"]
|
|
709
711
|
self._status_click_managed = state["status_click_managed"]
|
|
710
712
|
self._termux_tap_managed = state["termux_tap_managed"]
|
|
713
|
+
self._registered = True
|
|
711
714
|
return True
|
|
712
715
|
except OSError:
|
|
713
716
|
return False
|
|
@@ -645,3 +645,53 @@ def launcher_argv(
|
|
|
645
645
|
"--inside-tmux",
|
|
646
646
|
*forwarded_args,
|
|
647
647
|
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def ensure_detached_launcher_session(
|
|
651
|
+
target: TmuxServerTarget,
|
|
652
|
+
launch_prefix: Sequence[str],
|
|
653
|
+
forwarded_args: Sequence[str],
|
|
654
|
+
*,
|
|
655
|
+
env: Mapping[str, str] | None = None,
|
|
656
|
+
timeout: float = 5.0,
|
|
657
|
+
) -> str | None:
|
|
658
|
+
"""Create the outer UI detached on one already-proven server.
|
|
659
|
+
|
|
660
|
+
Managed Windows uses this only when a proven existing server has no outer
|
|
661
|
+
UI, commonly because Soft Quit left provider sessions alive in another
|
|
662
|
+
Terminal Services session. A new tmux client can then reject
|
|
663
|
+
``new-session -A`` before the outer session exists, so the server-origin
|
|
664
|
+
terminal bridge has no immutable session to attach.
|
|
665
|
+
|
|
666
|
+
An exact pre/post server-PID check fences the mutation. A concurrent
|
|
667
|
+
launcher may win the create race; resolving one exact resulting session is
|
|
668
|
+
success regardless of this client's ``new-session`` return code.
|
|
669
|
+
"""
|
|
670
|
+
existing = target_session_id(target, "railmux", timeout=0.5)
|
|
671
|
+
if existing is not None:
|
|
672
|
+
return existing
|
|
673
|
+
if not launch_prefix or not target_is_live(target, timeout=1.0, env=env):
|
|
674
|
+
return None
|
|
675
|
+
try:
|
|
676
|
+
subprocess.run(
|
|
677
|
+
target_argv(
|
|
678
|
+
target,
|
|
679
|
+
"new-session",
|
|
680
|
+
"-d",
|
|
681
|
+
"-s",
|
|
682
|
+
"railmux",
|
|
683
|
+
*launch_prefix,
|
|
684
|
+
"--inside-tmux",
|
|
685
|
+
*forwarded_args,
|
|
686
|
+
),
|
|
687
|
+
stdout=subprocess.DEVNULL,
|
|
688
|
+
stderr=subprocess.DEVNULL,
|
|
689
|
+
timeout=timeout,
|
|
690
|
+
check=False,
|
|
691
|
+
env=None if env is None else dict(env),
|
|
692
|
+
)
|
|
693
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
694
|
+
return None
|
|
695
|
+
if not target_is_live(target, timeout=1.0, env=env):
|
|
696
|
+
return None
|
|
697
|
+
return target_session_id(target, "railmux", timeout=1.0)
|
|
@@ -125,6 +125,20 @@ _CODEX_ROLLOUT_PROBE_TTL_S = 2.0
|
|
|
125
125
|
_SESSION_LEASE_PROBE_TTL_S = 2.0
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def _tmux_batch_argv(commands: list[list[str]]) -> list[str]:
|
|
129
|
+
"""Encode independent tmux commands into one client invocation."""
|
|
130
|
+
argv = ["tmux"]
|
|
131
|
+
first = True
|
|
132
|
+
for command in commands:
|
|
133
|
+
if not command:
|
|
134
|
+
continue
|
|
135
|
+
if not first:
|
|
136
|
+
argv.append(";")
|
|
137
|
+
argv.extend(command)
|
|
138
|
+
first = False
|
|
139
|
+
return argv
|
|
140
|
+
|
|
141
|
+
|
|
128
142
|
PALETTE = [
|
|
129
143
|
# Status-bar message levels. Idle tips are dim; info neutral green;
|
|
130
144
|
# warn/error escalate so failures stand out from routine feedback.
|
|
@@ -769,6 +783,11 @@ class App:
|
|
|
769
783
|
self._delete_progress_text: str = "Deleting…"
|
|
770
784
|
self._rendered_status_text: str | None = None
|
|
771
785
|
self._rendered_status_level: str = "tip"
|
|
786
|
+
# Applying one logical status-bar frame used to spawn five tmux
|
|
787
|
+
# clients. Keep the last complete frame so repeated focus/resize
|
|
788
|
+
# callbacks do not rewrite identical options, which is especially
|
|
789
|
+
# expensive under MSYS2 on Windows.
|
|
790
|
+
self._applied_tmux_bar_state: tuple[str, str, str, int, int] | None = None
|
|
772
791
|
self._status_feedback_alarm: object | None = None
|
|
773
792
|
self._attention_notice_key: tuple[str, int] | None = None
|
|
774
793
|
self._tip_index: int = 0
|
|
@@ -857,6 +876,16 @@ class App:
|
|
|
857
876
|
identity.server_digest, identity.pane_id)
|
|
858
877
|
if identity is not None else None
|
|
859
878
|
)
|
|
879
|
+
# MSYS2 pays tens of milliseconds for every tmux client process. The
|
|
880
|
+
# crash-safe global wheel/function leases are independent from provider
|
|
881
|
+
# discovery and the first sidebar frame, so Windows prepares them on a
|
|
882
|
+
# background worker while the UI becomes interactive. Teardown joins
|
|
883
|
+
# that worker before restoring any lease.
|
|
884
|
+
self._windows_tmux_setup_lock = threading.Lock()
|
|
885
|
+
self._windows_tmux_setup_thread: threading.Thread | None = None
|
|
886
|
+
self._windows_tmux_setup_result: tuple[bool, bool] | None = None
|
|
887
|
+
self._windows_tmux_setup_done = False
|
|
888
|
+
self._windows_tmux_setup_cancelled = False
|
|
860
889
|
self._termux_local_touch = bool(
|
|
861
890
|
identity is not None
|
|
862
891
|
and is_termux_environment()
|
|
@@ -2467,8 +2496,16 @@ class App:
|
|
|
2467
2496
|
|
|
2468
2497
|
def _install_tmux_bindings(self) -> None:
|
|
2469
2498
|
"""Ensure global forwarding and the current Target projection exist."""
|
|
2499
|
+
worker = getattr(self, "_windows_tmux_setup_thread", None)
|
|
2500
|
+
if worker is not None and worker.is_alive():
|
|
2501
|
+
return
|
|
2470
2502
|
manager = getattr(self, "_tmux_binding_manager", None)
|
|
2471
2503
|
opened = manager is not None and manager.open()
|
|
2504
|
+
self._finish_tmux_bindings(opened)
|
|
2505
|
+
|
|
2506
|
+
def _finish_tmux_bindings(self, opened: bool) -> None:
|
|
2507
|
+
"""Project pane-local state after the shared lease is prepared."""
|
|
2508
|
+
manager = getattr(self, "_tmux_binding_manager", None)
|
|
2472
2509
|
selection = getattr(self, "_selection_isolation_manager", None)
|
|
2473
2510
|
if selection is not None:
|
|
2474
2511
|
selection.sync(
|
|
@@ -2501,6 +2538,96 @@ class App:
|
|
|
2501
2538
|
)
|
|
2502
2539
|
self._sync_termux_tap_route()
|
|
2503
2540
|
|
|
2541
|
+
def _start_windows_tmux_setup(self) -> None:
|
|
2542
|
+
"""Prepare expensive shared tmux leases without delaying first paint."""
|
|
2543
|
+
if not running_in_windows_wrapper():
|
|
2544
|
+
return
|
|
2545
|
+
current = getattr(self, "_windows_tmux_setup_thread", None)
|
|
2546
|
+
if current is not None:
|
|
2547
|
+
return
|
|
2548
|
+
wheel = getattr(self, "_root_wheel_manager", None)
|
|
2549
|
+
manager = getattr(self, "_tmux_binding_manager", None)
|
|
2550
|
+
|
|
2551
|
+
def cancelled() -> bool:
|
|
2552
|
+
with self._windows_tmux_setup_lock:
|
|
2553
|
+
return self._windows_tmux_setup_cancelled
|
|
2554
|
+
|
|
2555
|
+
def prepare() -> None:
|
|
2556
|
+
try:
|
|
2557
|
+
if cancelled():
|
|
2558
|
+
return
|
|
2559
|
+
try:
|
|
2560
|
+
wheel_ok = wheel is None or wheel.open()
|
|
2561
|
+
except Exception:
|
|
2562
|
+
# These leases improve mouse/status routing but are not
|
|
2563
|
+
# authority for provider discovery or history. Degrade to
|
|
2564
|
+
# the existing keyboard paths if an unexpected adapter
|
|
2565
|
+
# error escapes a fail-closed manager.
|
|
2566
|
+
wheel_ok = False
|
|
2567
|
+
if cancelled():
|
|
2568
|
+
return
|
|
2569
|
+
try:
|
|
2570
|
+
bindings_opened = manager is not None and manager.open()
|
|
2571
|
+
except Exception:
|
|
2572
|
+
bindings_opened = False
|
|
2573
|
+
with self._windows_tmux_setup_lock:
|
|
2574
|
+
if not self._windows_tmux_setup_cancelled:
|
|
2575
|
+
self._windows_tmux_setup_result = (
|
|
2576
|
+
wheel_ok, bindings_opened)
|
|
2577
|
+
finally:
|
|
2578
|
+
# Publish terminal completion even for a BaseException so the
|
|
2579
|
+
# Urwid callback cannot poll a dead worker forever.
|
|
2580
|
+
with self._windows_tmux_setup_lock:
|
|
2581
|
+
self._windows_tmux_setup_done = True
|
|
2582
|
+
|
|
2583
|
+
worker = threading.Thread(
|
|
2584
|
+
target=prepare,
|
|
2585
|
+
name="railmux-windows-tmux-setup",
|
|
2586
|
+
daemon=False,
|
|
2587
|
+
)
|
|
2588
|
+
self._windows_tmux_setup_thread = worker
|
|
2589
|
+
worker.start()
|
|
2590
|
+
|
|
2591
|
+
def _finish_windows_tmux_setup(self, loop, _user_data) -> None:
|
|
2592
|
+
"""Consume background lease setup on the Urwid thread."""
|
|
2593
|
+
with self._windows_tmux_setup_lock:
|
|
2594
|
+
result = self._windows_tmux_setup_result
|
|
2595
|
+
done = self._windows_tmux_setup_done
|
|
2596
|
+
if result is not None:
|
|
2597
|
+
self._windows_tmux_setup_result = None
|
|
2598
|
+
if result is None:
|
|
2599
|
+
worker = getattr(self, "_windows_tmux_setup_thread", None)
|
|
2600
|
+
if not done and worker is not None and worker.is_alive():
|
|
2601
|
+
loop.set_alarm_in(0.05, self._finish_windows_tmux_setup)
|
|
2602
|
+
elif not done:
|
|
2603
|
+
# No worker was created, or it died before publishing its
|
|
2604
|
+
# terminal state. Preserve the pre-deferral one-shot setup
|
|
2605
|
+
# without leaving a 20 Hz alarm behind.
|
|
2606
|
+
self._install_tmux_bindings()
|
|
2607
|
+
return
|
|
2608
|
+
wheel_ok, bindings_opened = result
|
|
2609
|
+
if not wheel_ok:
|
|
2610
|
+
self._set_status(
|
|
2611
|
+
"Mouse-wheel forwarding unavailable; tmux may have custom "
|
|
2612
|
+
"root wheel bindings.",
|
|
2613
|
+
"warn",
|
|
2614
|
+
)
|
|
2615
|
+
self._finish_tmux_bindings(bindings_opened)
|
|
2616
|
+
|
|
2617
|
+
def _cancel_windows_tmux_setup(self) -> None:
|
|
2618
|
+
"""Stop starting leases and wait before main-thread cleanup."""
|
|
2619
|
+
worker = getattr(self, "_windows_tmux_setup_thread", None)
|
|
2620
|
+
if worker is None:
|
|
2621
|
+
return
|
|
2622
|
+
with self._windows_tmux_setup_lock:
|
|
2623
|
+
self._windows_tmux_setup_cancelled = True
|
|
2624
|
+
# Waiting here gives teardown sole cleanup ownership and prevents a
|
|
2625
|
+
# late worker write from reinstalling bindings after their originals
|
|
2626
|
+
# were restored. These same manager operations ran synchronously before
|
|
2627
|
+
# deferral; a non-daemon worker keeps that cleanup guarantee if a later
|
|
2628
|
+
# teardown phase raises.
|
|
2629
|
+
worker.join()
|
|
2630
|
+
|
|
2504
2631
|
def _set_workspace_target(self, slot_key: str) -> AgentSlot:
|
|
2505
2632
|
"""Apply one Target transition and refresh its tmux projection."""
|
|
2506
2633
|
slot = self._agent_workspace().set_target(slot_key)
|
|
@@ -3062,6 +3189,19 @@ class App:
|
|
|
3062
3189
|
workspace = self._agent_workspace()
|
|
3063
3190
|
if workspace.presentation is presentation:
|
|
3064
3191
|
return True
|
|
3192
|
+
has_agent_page = any(slot.pane_id for slot in workspace.slots)
|
|
3193
|
+
if not has_agent_page:
|
|
3194
|
+
# With only the sidebar pane there is no tmux page to select,
|
|
3195
|
+
# zoom, or preserve. Avoid a chain of geometry subprocesses on
|
|
3196
|
+
# initial compact terminals; the logical presentation still
|
|
3197
|
+
# drives the responsive footer and status bar.
|
|
3198
|
+
workspace.presentation = presentation
|
|
3199
|
+
workspace.compact_page = WorkspacePage.SIDEBAR
|
|
3200
|
+
self._pre_compact_wide_zoom_pane = None
|
|
3201
|
+
self._pre_compact_layout_profile = None
|
|
3202
|
+
self._remote_compact_prepared = None
|
|
3203
|
+
self._apply_tmux_bar(self._tmux_error_bar)
|
|
3204
|
+
return True
|
|
3065
3205
|
if presentation is WorkspacePresentation.COMPACT:
|
|
3066
3206
|
owner = getattr(self, "_railmux_pane_id", None)
|
|
3067
3207
|
prepared = getattr(self, "_remote_compact_prepared", None)
|
|
@@ -8099,6 +8239,7 @@ class App:
|
|
|
8099
8239
|
if isinstance(index, BackgroundCodexIndex):
|
|
8100
8240
|
index.close(timeout_s=0.2)
|
|
8101
8241
|
self._teardown_scroll_acceleration()
|
|
8242
|
+
self._cancel_windows_tmux_setup()
|
|
8102
8243
|
selection = getattr(self, "_selection_isolation_manager", None)
|
|
8103
8244
|
if selection is not None:
|
|
8104
8245
|
selection.close()
|
|
@@ -10559,7 +10700,9 @@ class App:
|
|
|
10559
10700
|
elif (size_changed and workspace.presentation
|
|
10560
10701
|
is WorkspacePresentation.COMPACT):
|
|
10561
10702
|
self._apply_tmux_bar(self._tmux_error_bar)
|
|
10703
|
+
has_agent_page = any(slot.pane_id for slot in workspace.slots)
|
|
10562
10704
|
if (workspace.presentation is WorkspacePresentation.COMPACT
|
|
10705
|
+
and has_agent_page
|
|
10563
10706
|
and self._window_is_zoomed() is False):
|
|
10564
10707
|
# Retry a transient failed zoom and heal manual/unexpected unzoom;
|
|
10565
10708
|
# compact presentation is defined by exactly one visible page.
|
|
@@ -10795,16 +10938,16 @@ class App:
|
|
|
10795
10938
|
self._rendered_status_level = level
|
|
10796
10939
|
try:
|
|
10797
10940
|
import subprocess as _sp
|
|
10798
|
-
|
|
10799
|
-
["
|
|
10941
|
+
commands = [
|
|
10942
|
+
["set-option", "-t", self._tmux_status_session,
|
|
10800
10943
|
"status-right", payload],
|
|
10944
|
+
]
|
|
10945
|
+
if refresh:
|
|
10946
|
+
commands.append(["refresh-client", "-S"])
|
|
10947
|
+
_sp.run(
|
|
10948
|
+
_tmux_batch_argv(commands),
|
|
10801
10949
|
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
10802
10950
|
)
|
|
10803
|
-
if refresh:
|
|
10804
|
-
_sp.run(
|
|
10805
|
-
["tmux", "refresh-client", "-S"],
|
|
10806
|
-
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
10807
|
-
)
|
|
10808
10951
|
except Exception:
|
|
10809
10952
|
pass
|
|
10810
10953
|
|
|
@@ -10880,23 +11023,35 @@ class App:
|
|
|
10880
11023
|
self._status_layout_indicator(),
|
|
10881
11024
|
wrap_action,
|
|
10882
11025
|
)
|
|
11026
|
+
state = (
|
|
11027
|
+
self._tmux_status_session,
|
|
11028
|
+
bar,
|
|
11029
|
+
brand,
|
|
11030
|
+
left_length,
|
|
11031
|
+
right_length,
|
|
11032
|
+
)
|
|
11033
|
+
if getattr(self, "_applied_tmux_bar_state", None) == state:
|
|
11034
|
+
return
|
|
10883
11035
|
try:
|
|
10884
11036
|
import subprocess as _sp
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
["tmux", "set-option", "-t", self._tmux_status_session,
|
|
10893
|
-
opt, val],
|
|
10894
|
-
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
11037
|
+
commands = [
|
|
11038
|
+
["set-option", "-t", self._tmux_status_session, opt, val]
|
|
11039
|
+
for opt, val in (
|
|
11040
|
+
("status-style", bar),
|
|
11041
|
+
("status-left", brand),
|
|
11042
|
+
("status-left-length", str(left_length)),
|
|
11043
|
+
("status-right-length", str(right_length)),
|
|
10895
11044
|
)
|
|
11045
|
+
]
|
|
10896
11046
|
# Force an immediate repaint (default status-interval is 15s) so a
|
|
10897
11047
|
# mode toggle or error flip shows at once, not on the next tick.
|
|
10898
|
-
|
|
10899
|
-
|
|
11048
|
+
commands.append(["refresh-client", "-S"])
|
|
11049
|
+
result = _sp.run(
|
|
11050
|
+
_tmux_batch_argv(commands),
|
|
11051
|
+
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
11052
|
+
)
|
|
11053
|
+
if getattr(result, "returncode", 0) == 0:
|
|
11054
|
+
self._applied_tmux_bar_state = state
|
|
10900
11055
|
except Exception:
|
|
10901
11056
|
pass
|
|
10902
11057
|
|
|
@@ -11040,28 +11195,31 @@ class App:
|
|
|
11040
11195
|
if tmux_ctl.in_tmux():
|
|
11041
11196
|
sess = tmux_ctl.current_session_name() or "railmux"
|
|
11042
11197
|
_sp.run(
|
|
11043
|
-
[
|
|
11198
|
+
_tmux_batch_argv([
|
|
11199
|
+
["set-option", "-t", sess, "set-clipboard", "on"],
|
|
11200
|
+
["set-option", "-t", sess, "focus-events", "on"],
|
|
11201
|
+
["set-option", "-t", sess, "mouse", "on"],
|
|
11202
|
+
]),
|
|
11044
11203
|
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
11045
11204
|
)
|
|
11046
11205
|
# Force OSC 52 passthrough so a left-drag selection in either pane
|
|
11047
11206
|
# copies to the *local* system clipboard (works over SSH / nested
|
|
11048
11207
|
# tmux on OSC-52-capable terminals). Pairs with set-clipboard on.
|
|
11049
11208
|
tmux_ctl.enable_clipboard_passthrough()
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
)
|
|
11209
|
+
if running_in_windows_wrapper():
|
|
11210
|
+
# Root-table lease discovery is particularly expensive in
|
|
11211
|
+
# MSYS2 because each tmux query launches a Windows process.
|
|
11212
|
+
# It is independent of provider/session restoration, so
|
|
11213
|
+
# finish it after the first interactive frame.
|
|
11214
|
+
self._start_windows_tmux_setup()
|
|
11215
|
+
else:
|
|
11216
|
+
wheel = getattr(self, "_root_wheel_manager", None)
|
|
11217
|
+
if wheel is not None and not wheel.open():
|
|
11218
|
+
self._set_status(
|
|
11219
|
+
"Mouse-wheel forwarding unavailable; tmux may "
|
|
11220
|
+
"have custom root wheel bindings.",
|
|
11221
|
+
"warn",
|
|
11222
|
+
)
|
|
11065
11223
|
self._railmux_pane_id = tmux_ctl.current_pane_id()
|
|
11066
11224
|
if (self._railmux_pane_id is not None
|
|
11067
11225
|
and tmux_ctl.current_session_name() == "railmux"
|
|
@@ -11077,7 +11235,8 @@ class App:
|
|
|
11077
11235
|
# a saved agent-focused workspace will replace that temporary
|
|
11078
11236
|
# topology shortly. Do not expose its misleading row focus.
|
|
11079
11237
|
self._frame.set_window_active(False)
|
|
11080
|
-
self
|
|
11238
|
+
if getattr(self, "_windows_tmux_setup_thread", None) is None:
|
|
11239
|
+
self._install_tmux_bindings()
|
|
11081
11240
|
# bracketed_paste_mode: the terminal frames pastes in begin/end markers
|
|
11082
11241
|
# so _filter_input can drop them — sidebar keys are destructive commands,
|
|
11083
11242
|
# not text input.
|
|
@@ -11111,11 +11270,13 @@ class App:
|
|
|
11111
11270
|
# visually clean. These overrides remain session-scoped and
|
|
11112
11271
|
# teardown reverts them to the dedicated server's hidden
|
|
11113
11272
|
# baseline.
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
["
|
|
11117
|
-
|
|
11118
|
-
)
|
|
11273
|
+
_sp.run(
|
|
11274
|
+
_tmux_batch_argv([
|
|
11275
|
+
["set-option", "-t", sess, opt, val]
|
|
11276
|
+
for opt, val in self._TMUX_BAR_OPTIONS
|
|
11277
|
+
]),
|
|
11278
|
+
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
11279
|
+
)
|
|
11119
11280
|
self._tmux_status_session = sess
|
|
11120
11281
|
self._tmux_status_enabled = True
|
|
11121
11282
|
self._apply_tmux_bar(error=False) # initial green bar
|
|
@@ -11144,6 +11305,12 @@ class App:
|
|
|
11144
11305
|
if running_in_windows_wrapper():
|
|
11145
11306
|
self._loop.set_alarm_in(
|
|
11146
11307
|
0.05, self._report_windows_startup_summary)
|
|
11308
|
+
# Leave one bounded initial-paint interval before consuming
|
|
11309
|
+
# the lease result. Even if the worker completed immediately,
|
|
11310
|
+
# its status/bar projection cannot delay the first frame.
|
|
11311
|
+
if self._windows_tmux_setup_thread is not None:
|
|
11312
|
+
self._loop.set_alarm_in(
|
|
11313
|
+
0.1, self._finish_windows_tmux_setup)
|
|
11147
11314
|
self._loop.run()
|
|
11148
11315
|
except KeyboardInterrupt:
|
|
11149
11316
|
# Ctrl-C / SIGINT — fall through to teardown.
|
|
@@ -472,6 +472,73 @@ def test_prelaunch_recovery_is_scoped_to_the_dedicated_server(monkeypatch):
|
|
|
472
472
|
assert os.environ["TMUX_PANE"] == "%9"
|
|
473
473
|
|
|
474
474
|
|
|
475
|
+
def test_managed_windows_precreates_missing_outer_session_for_bridge(
|
|
476
|
+
monkeypatch,
|
|
477
|
+
):
|
|
478
|
+
target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
|
|
479
|
+
monkeypatch.setattr(
|
|
480
|
+
"railmux.cli.tmux_server.discover_target", lambda: target)
|
|
481
|
+
monkeypatch.setattr(
|
|
482
|
+
"railmux.cli.tmux_server.is_current_server", lambda _target: False)
|
|
483
|
+
monkeypatch.setattr(
|
|
484
|
+
"railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
|
|
485
|
+
monkeypatch.setattr("railmux.cli.sys.argv", ["/opt/railmux/bin/railmux"])
|
|
486
|
+
monkeypatch.setattr(
|
|
487
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
488
|
+
lambda: True,
|
|
489
|
+
)
|
|
490
|
+
prepared = MagicMock(return_value="$9")
|
|
491
|
+
monkeypatch.setattr(
|
|
492
|
+
"railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
|
|
493
|
+
run_client = MagicMock(return_value=0)
|
|
494
|
+
monkeypatch.setattr(
|
|
495
|
+
"railmux.cli._run_tmux_client_with_watchdog", run_client)
|
|
496
|
+
|
|
497
|
+
assert main(["--project", "/work"]) == 0
|
|
498
|
+
|
|
499
|
+
client_env = tmux_server.exec_environment()
|
|
500
|
+
prepared.assert_called_once_with(
|
|
501
|
+
target,
|
|
502
|
+
["/opt/railmux/bin/railmux"],
|
|
503
|
+
["--project", "/work"],
|
|
504
|
+
env=client_env,
|
|
505
|
+
)
|
|
506
|
+
assert run_client.call_args.kwargs == {
|
|
507
|
+
"expected_target": target,
|
|
508
|
+
"expected_session_id": "$9",
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_posix_missing_outer_session_keeps_ordinary_new_session_path(
|
|
513
|
+
monkeypatch,
|
|
514
|
+
):
|
|
515
|
+
target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
|
|
516
|
+
monkeypatch.setattr(
|
|
517
|
+
"railmux.cli.tmux_server.discover_target", lambda: target)
|
|
518
|
+
monkeypatch.setattr(
|
|
519
|
+
"railmux.cli.tmux_server.is_current_server", lambda _target: False)
|
|
520
|
+
monkeypatch.setattr(
|
|
521
|
+
"railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
|
|
522
|
+
monkeypatch.setattr(
|
|
523
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
524
|
+
lambda: False,
|
|
525
|
+
)
|
|
526
|
+
prepared = MagicMock()
|
|
527
|
+
monkeypatch.setattr(
|
|
528
|
+
"railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
|
|
529
|
+
run_client = MagicMock(return_value=0)
|
|
530
|
+
monkeypatch.setattr(
|
|
531
|
+
"railmux.cli._run_tmux_client_with_watchdog", run_client)
|
|
532
|
+
|
|
533
|
+
assert main([]) == 0
|
|
534
|
+
|
|
535
|
+
prepared.assert_not_called()
|
|
536
|
+
assert run_client.call_args.kwargs == {
|
|
537
|
+
"expected_target": target,
|
|
538
|
+
"expected_session_id": None,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
475
542
|
def test_local_tmux_watchdog_exits_and_records_after_consecutive_failures(
|
|
476
543
|
monkeypatch, capsys,
|
|
477
544
|
):
|
|
@@ -3576,18 +3576,50 @@ def test_periodic_prefetch_never_moves_an_existing_frozen_viewport():
|
|
|
3576
3576
|
|
|
3577
3577
|
assert action == HistoryAction()
|
|
3578
3578
|
assert view.overlays() == before
|
|
3579
|
-
# A redraw without safe anchors
|
|
3580
|
-
#
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3579
|
+
# A redraw without safe anchors cannot be spliced onto the old capture:
|
|
3580
|
+
# doing so would manufacture a false seam and omit intermediate rows. The
|
|
3581
|
+
# immutable frozen viewport remains visible, while the next history entry
|
|
3582
|
+
# starts from the newest internally contiguous capture.
|
|
3583
|
+
assert view.content_cache["%8"].lines == advanced.lines
|
|
3584
|
+
|
|
3585
|
+
|
|
3586
|
+
def test_unaligned_hot_prefetch_never_creates_a_discontinuous_history_seam():
|
|
3587
|
+
view = LocalHistoryView(history_limit=2000)
|
|
3588
|
+
first = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
|
|
3589
|
+
first_id, _limit = decode_history_prefetch(first.data)
|
|
3590
|
+
old = HistorySnapshot(
|
|
3591
|
+
first_id,
|
|
3592
|
+
"%8",
|
|
3593
|
+
30,
|
|
3594
|
+
0,
|
|
3595
|
+
30,
|
|
3596
|
+
3,
|
|
3597
|
+
tuple(f"old-{index}".encode() for index in range(300)),
|
|
3598
|
+
more_available=True,
|
|
3599
|
+
)
|
|
3600
|
+
view.accept_prefetch(HistoryBatch(first_id, (old,)))
|
|
3601
|
+
|
|
3602
|
+
second = InputFrameDecoder().feed(view.begin_prefetch(2.0))[0]
|
|
3603
|
+
second_id, _limit = decode_history_prefetch(second.data)
|
|
3604
|
+
current = HistorySnapshot(
|
|
3605
|
+
second_id,
|
|
3606
|
+
"%8",
|
|
3607
|
+
30,
|
|
3608
|
+
0,
|
|
3609
|
+
30,
|
|
3610
|
+
3,
|
|
3611
|
+
tuple(f"new-{index}".encode() for index in range(300)),
|
|
3612
|
+
more_available=True,
|
|
3590
3613
|
)
|
|
3614
|
+
view.accept_prefetch(HistoryBatch(second_id, (current,)))
|
|
3615
|
+
|
|
3616
|
+
wheel = view.wheel(SgrMouseEvent(b"up", 64, 40, 2, True))
|
|
3617
|
+
|
|
3618
|
+
assert view.viewports["%8"].snapshot.lines == current.lines
|
|
3619
|
+
assert view.overlays()[0][1] == (b"new-296", b"new-297", b"new-298")
|
|
3620
|
+
assert b"old-296" not in view.viewports["%8"].snapshot.lines
|
|
3621
|
+
request = InputFrameDecoder().feed(wheel.protocol_frame)[0]
|
|
3622
|
+
assert decode_history_request(request.data)[3] == 2000
|
|
3591
3623
|
|
|
3592
3624
|
|
|
3593
3625
|
def test_temporary_blank_fullscreen_tail_is_removed_after_main_view_returns():
|