railmux 0.4.0.dev33__tar.gz → 0.4.0.dev34__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.dev33/src/railmux.egg-info → railmux-0.4.0.dev34}/PKG-INFO +1 -1
- railmux-0.4.0.dev34/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/display_transport.py +308 -0
- railmux-0.4.0.dev34/src/railmux/preview_pager.py +53 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/tmux_ctl.py +257 -46
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/app.py +15 -18
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_codex_delete.py +7 -11
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_display_transport.py +163 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_focus_highlight.py +40 -1
- railmux-0.4.0.dev34/tests/test_preview_pager.py +42 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tmux_ctl.py +24 -7
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tmux_integration.py +49 -0
- railmux-0.4.0.dev33/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/LICENSE +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/README.md +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/pyproject.toml +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/setup.cfg +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.0.dev33 → railmux-0.4.0.dev34}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev34"
|
|
@@ -48,6 +48,8 @@ class AttachOutcome:
|
|
|
48
48
|
kind: DisplayTransportKind
|
|
49
49
|
reason: str | None = None
|
|
50
50
|
fell_back: bool = False
|
|
51
|
+
display_stable: bool = False
|
|
52
|
+
target_unchanged: bool = False
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
@dataclass(frozen=True)
|
|
@@ -178,6 +180,90 @@ def _verified_displayed(state: SwapState) -> bool:
|
|
|
178
180
|
)
|
|
179
181
|
|
|
180
182
|
|
|
183
|
+
def _snapshot_verified_home(
|
|
184
|
+
snapshot: tmux_ctl.TmuxStateSnapshot,
|
|
185
|
+
state: SwapState,
|
|
186
|
+
) -> bool:
|
|
187
|
+
real = snapshot.pane(state.agent_pane_id)
|
|
188
|
+
placeholder = snapshot.pane(state.placeholder_pane_id)
|
|
189
|
+
topology = snapshot.topology(state.agent_tmux_name)
|
|
190
|
+
return bool(
|
|
191
|
+
real is not None
|
|
192
|
+
and placeholder is not None
|
|
193
|
+
and topology is not None
|
|
194
|
+
and real.pane_pid == state.agent_pane_pid
|
|
195
|
+
and real.window_id == state.home_window_id
|
|
196
|
+
and placeholder.window_id == state.display_window_id
|
|
197
|
+
and state.home_window_id in topology.window_ids
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _snapshot_verified_displayed(
|
|
202
|
+
snapshot: tmux_ctl.TmuxStateSnapshot,
|
|
203
|
+
state: SwapState,
|
|
204
|
+
) -> bool:
|
|
205
|
+
real = snapshot.pane(state.agent_pane_id)
|
|
206
|
+
placeholder = snapshot.pane(state.placeholder_pane_id)
|
|
207
|
+
topology = snapshot.topology(state.agent_tmux_name)
|
|
208
|
+
return bool(
|
|
209
|
+
real is not None
|
|
210
|
+
and placeholder is not None
|
|
211
|
+
and topology is not None
|
|
212
|
+
and real.pane_pid == state.agent_pane_pid
|
|
213
|
+
and real.window_id == state.display_window_id
|
|
214
|
+
and placeholder.window_id == state.home_window_id
|
|
215
|
+
and topology.attached_clients == 0
|
|
216
|
+
and topology.single_live_pane is not None
|
|
217
|
+
and topology.single_live_pane.pane_id == state.placeholder_pane_id
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _snapshot_marker_gate(
|
|
222
|
+
snapshot: tmux_ctl.TmuxStateSnapshot,
|
|
223
|
+
agent_tmux_name: str,
|
|
224
|
+
known_transactions: frozenset[str],
|
|
225
|
+
) -> str | None:
|
|
226
|
+
for _window, marker, raw in snapshot.window_options:
|
|
227
|
+
if marker not in _MARKERS.values() or not raw:
|
|
228
|
+
continue
|
|
229
|
+
decoded = _decode_record(raw)
|
|
230
|
+
if decoded is None:
|
|
231
|
+
return "inconsistent swap recovery metadata exists"
|
|
232
|
+
state = decoded[1]
|
|
233
|
+
if (
|
|
234
|
+
state.agent_tmux_name == agent_tmux_name
|
|
235
|
+
and state.transaction_id not in known_transactions
|
|
236
|
+
):
|
|
237
|
+
return "agent pane is owned by another swap transaction"
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _marker_commands(
|
|
242
|
+
state: SwapState,
|
|
243
|
+
slot_key: str,
|
|
244
|
+
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
245
|
+
marker = _marker_for(slot_key)
|
|
246
|
+
raw = _encode_record(state, slot_key)
|
|
247
|
+
return (
|
|
248
|
+
("set-window-option", "-t", state.home_window_id, marker, raw),
|
|
249
|
+
("set-window-option", "-t", state.display_window_id, marker, raw),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _snapshot_has_marker_pair(
|
|
254
|
+
snapshot: tmux_ctl.TmuxStateSnapshot,
|
|
255
|
+
state: SwapState,
|
|
256
|
+
slot_key: str,
|
|
257
|
+
) -> bool:
|
|
258
|
+
marker = _marker_for(slot_key)
|
|
259
|
+
for window in (state.home_window_id, state.display_window_id):
|
|
260
|
+
raw = snapshot.window_option(window, marker)
|
|
261
|
+
decoded = _decode_record(raw) if raw else None
|
|
262
|
+
if decoded is None or decoded != (slot_key, state):
|
|
263
|
+
return False
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
|
|
181
267
|
class NestedDisplayTransport:
|
|
182
268
|
"""The compatibility transport: a nested tmux attach client."""
|
|
183
269
|
|
|
@@ -542,6 +628,224 @@ class AgentDisplayTransport:
|
|
|
542
628
|
return "agent pane is owned by another swap transaction"
|
|
543
629
|
return None
|
|
544
630
|
|
|
631
|
+
def _fast_switch_swap(
|
|
632
|
+
self,
|
|
633
|
+
slot: AgentSlot,
|
|
634
|
+
agent_tmux_name: str,
|
|
635
|
+
) -> AttachOutcome | None:
|
|
636
|
+
"""Switch an exact displayed swap in a bounded tmux transaction.
|
|
637
|
+
|
|
638
|
+
``None`` means the optimized gate declined before making an
|
|
639
|
+
irreversible change and the established path should continue. Every
|
|
640
|
+
mutation remains journaled and is followed by a coherent identity
|
|
641
|
+
snapshot; a failed or ambiguous mutation returns an explicit failure
|
|
642
|
+
with recovery metadata retained.
|
|
643
|
+
"""
|
|
644
|
+
old = slot.swap_state
|
|
645
|
+
if (
|
|
646
|
+
old is None
|
|
647
|
+
or slot.transport_kind is not DisplayTransportKind.SWAP
|
|
648
|
+
or slot.display_parked
|
|
649
|
+
or tmux_ctl.tmux_version() < (3, 1)
|
|
650
|
+
):
|
|
651
|
+
return None
|
|
652
|
+
marker_names = tuple(_MARKERS.values())
|
|
653
|
+
before = tmux_ctl.tmux_state_snapshot(marker_names)
|
|
654
|
+
if before is None or not _snapshot_verified_displayed(before, old):
|
|
655
|
+
return None
|
|
656
|
+
if not _snapshot_has_marker_pair(before, old, slot.key):
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
# Clicking the already displayed row is an identity revalidation, not
|
|
660
|
+
# a reason to rewrite markers, resize windows, or move panes.
|
|
661
|
+
if old.agent_tmux_name == agent_tmux_name:
|
|
662
|
+
return AttachOutcome(
|
|
663
|
+
True,
|
|
664
|
+
DisplayTransportKind.SWAP,
|
|
665
|
+
display_stable=True,
|
|
666
|
+
target_unchanged=True,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
known = frozenset(
|
|
670
|
+
state.transaction_id
|
|
671
|
+
for candidate in self.workspace.slots
|
|
672
|
+
if (state := candidate.swap_state) is not None
|
|
673
|
+
)
|
|
674
|
+
if _snapshot_marker_gate(
|
|
675
|
+
before, agent_tmux_name, known) is not None:
|
|
676
|
+
return None
|
|
677
|
+
if any(
|
|
678
|
+
candidate is not slot
|
|
679
|
+
and candidate.agent_tmux_name == agent_tmux_name
|
|
680
|
+
for candidate in self.workspace.slots
|
|
681
|
+
):
|
|
682
|
+
return None
|
|
683
|
+
target_topology = before.topology(agent_tmux_name)
|
|
684
|
+
target_real = (
|
|
685
|
+
target_topology.single_live_pane
|
|
686
|
+
if target_topology is not None
|
|
687
|
+
and target_topology.attached_clients == 0
|
|
688
|
+
else None
|
|
689
|
+
)
|
|
690
|
+
if target_real is None or any(
|
|
691
|
+
candidate is not slot
|
|
692
|
+
and candidate.swap_state is not None
|
|
693
|
+
and candidate.swap_state.agent_pane_id == target_real.pane_id
|
|
694
|
+
for candidate in self.workspace.slots
|
|
695
|
+
):
|
|
696
|
+
return None
|
|
697
|
+
|
|
698
|
+
old_real = before.pane(old.agent_pane_id)
|
|
699
|
+
keeper = before.topology(old.keeper_session)
|
|
700
|
+
if (
|
|
701
|
+
old_real is None
|
|
702
|
+
or keeper is None
|
|
703
|
+
or keeper.session_id != old.keeper_session_id
|
|
704
|
+
or old.display_window_id not in keeper.window_ids
|
|
705
|
+
):
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
returning = self._with_phase(old, "returning")
|
|
709
|
+
return_commands = (
|
|
710
|
+
*_marker_commands(returning, slot.key),
|
|
711
|
+
(
|
|
712
|
+
"resize-window", "-t", old.agent_tmux_name,
|
|
713
|
+
"-x", str(old_real.width), "-y", str(old_real.height),
|
|
714
|
+
),
|
|
715
|
+
(
|
|
716
|
+
"swap-pane", "-d", "-s", old.agent_pane_id,
|
|
717
|
+
"-t", old.placeholder_pane_id,
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
# The coherent postcondition is authoritative even if the client loses
|
|
721
|
+
# its final acknowledgement after tmux completed the atomic pane move.
|
|
722
|
+
tmux_ctl.run_command_queue(return_commands)
|
|
723
|
+
after_home = tmux_ctl.tmux_state_snapshot(marker_names)
|
|
724
|
+
if after_home is None or not _snapshot_verified_home(after_home, old):
|
|
725
|
+
if (
|
|
726
|
+
after_home is not None
|
|
727
|
+
and _snapshot_verified_displayed(after_home, old)
|
|
728
|
+
):
|
|
729
|
+
_write_marker_pair(old, slot.key)
|
|
730
|
+
return None
|
|
731
|
+
return AttachOutcome(
|
|
732
|
+
False,
|
|
733
|
+
DisplayTransportKind.SWAP,
|
|
734
|
+
"could not verify the previous agent at home; "
|
|
735
|
+
"recovery metadata was retained",
|
|
736
|
+
)
|
|
737
|
+
target_topology = after_home.topology(agent_tmux_name)
|
|
738
|
+
target_real = (
|
|
739
|
+
target_topology.single_live_pane
|
|
740
|
+
if target_topology is not None
|
|
741
|
+
and target_topology.attached_clients == 0
|
|
742
|
+
else None
|
|
743
|
+
)
|
|
744
|
+
placeholder = after_home.pane(old.placeholder_pane_id)
|
|
745
|
+
keeper = after_home.topology(old.keeper_session)
|
|
746
|
+
if (
|
|
747
|
+
target_real is None
|
|
748
|
+
or placeholder is None
|
|
749
|
+
or placeholder.window_id != old.display_window_id
|
|
750
|
+
or keeper is None
|
|
751
|
+
or keeper.session_id != old.keeper_session_id
|
|
752
|
+
or old.display_window_id not in keeper.window_ids
|
|
753
|
+
or _snapshot_marker_gate(
|
|
754
|
+
after_home, agent_tmux_name, known,
|
|
755
|
+
) is not None
|
|
756
|
+
):
|
|
757
|
+
# Old is already proven safe at home. The ordinary path can clear
|
|
758
|
+
# its matching marker and reevaluate the changed target topology.
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
prepared = SwapState(
|
|
762
|
+
transaction_id=uuid.uuid4().hex,
|
|
763
|
+
agent_tmux_name=agent_tmux_name,
|
|
764
|
+
agent_pane_id=target_real.pane_id,
|
|
765
|
+
agent_pane_pid=target_real.pane_pid,
|
|
766
|
+
home_window_id=target_real.window_id,
|
|
767
|
+
placeholder_pane_id=old.placeholder_pane_id,
|
|
768
|
+
display_window_id=old.display_window_id,
|
|
769
|
+
keeper_session=old.keeper_session,
|
|
770
|
+
keeper_session_id=old.keeper_session_id,
|
|
771
|
+
outer_session_name=old.outer_session_name,
|
|
772
|
+
outer_session_id=old.outer_session_id,
|
|
773
|
+
owner_pane_id=old.owner_pane_id,
|
|
774
|
+
phase="prepared",
|
|
775
|
+
)
|
|
776
|
+
marker = _marker_for(slot.key)
|
|
777
|
+
switch_commands = (
|
|
778
|
+
("set-window-option", "-t", old.home_window_id, "-u", marker),
|
|
779
|
+
("set-window-option", "-t", old.display_window_id, "-u", marker),
|
|
780
|
+
(
|
|
781
|
+
"set-option", "-p", "-t", old.placeholder_pane_id,
|
|
782
|
+
"-u", tmux_server.HISTORY_SOURCE_OPTION,
|
|
783
|
+
),
|
|
784
|
+
(
|
|
785
|
+
"set-option", "-p", "-t", old.placeholder_pane_id,
|
|
786
|
+
"-u", tmux_server.TRANSCRIPT_SOURCE_OPTION,
|
|
787
|
+
),
|
|
788
|
+
*_marker_commands(prepared, slot.key),
|
|
789
|
+
(
|
|
790
|
+
"swap-pane", "-d", "-s", prepared.agent_pane_id,
|
|
791
|
+
"-t", prepared.placeholder_pane_id,
|
|
792
|
+
),
|
|
793
|
+
)
|
|
794
|
+
tmux_ctl.run_guarded_window_transaction(
|
|
795
|
+
(
|
|
796
|
+
(old.home_window_id, marker, old.transaction_id),
|
|
797
|
+
(old.display_window_id, marker, old.transaction_id),
|
|
798
|
+
),
|
|
799
|
+
switch_commands,
|
|
800
|
+
)
|
|
801
|
+
after_switch = tmux_ctl.tmux_state_snapshot(marker_names)
|
|
802
|
+
if (
|
|
803
|
+
after_switch is None
|
|
804
|
+
or not _snapshot_verified_displayed(after_switch, prepared)
|
|
805
|
+
):
|
|
806
|
+
if (
|
|
807
|
+
after_switch is not None
|
|
808
|
+
and _snapshot_verified_home(after_switch, old)
|
|
809
|
+
and _snapshot_verified_home(after_switch, prepared)
|
|
810
|
+
):
|
|
811
|
+
_clear_markers(prepared, slot.key)
|
|
812
|
+
return None
|
|
813
|
+
return AttachOutcome(
|
|
814
|
+
False,
|
|
815
|
+
DisplayTransportKind.SWAP,
|
|
816
|
+
"optimized pane switch could not be verified; "
|
|
817
|
+
"recovery metadata was retained",
|
|
818
|
+
)
|
|
819
|
+
displayed = self._with_phase(prepared, "displayed")
|
|
820
|
+
committed = tmux_ctl.run_guarded_window_transaction(
|
|
821
|
+
(
|
|
822
|
+
(prepared.home_window_id, marker, prepared.transaction_id),
|
|
823
|
+
(
|
|
824
|
+
prepared.display_window_id,
|
|
825
|
+
marker,
|
|
826
|
+
prepared.transaction_id,
|
|
827
|
+
),
|
|
828
|
+
),
|
|
829
|
+
_marker_commands(displayed, slot.key),
|
|
830
|
+
)
|
|
831
|
+
slot.swap_state = displayed if committed else prepared
|
|
832
|
+
slot.pane_id = prepared.agent_pane_id
|
|
833
|
+
slot.transport_kind = DisplayTransportKind.SWAP
|
|
834
|
+
slot.agent_tmux_name = agent_tmux_name
|
|
835
|
+
slot.display_parked = False
|
|
836
|
+
if not committed:
|
|
837
|
+
return AttachOutcome(
|
|
838
|
+
False,
|
|
839
|
+
DisplayTransportKind.SWAP,
|
|
840
|
+
"pane switch committed but its recovery marker did not; "
|
|
841
|
+
"the verified pane was retained for recovery",
|
|
842
|
+
)
|
|
843
|
+
return AttachOutcome(
|
|
844
|
+
True,
|
|
845
|
+
DisplayTransportKind.SWAP,
|
|
846
|
+
display_stable=True,
|
|
847
|
+
)
|
|
848
|
+
|
|
545
849
|
def attach(
|
|
546
850
|
self,
|
|
547
851
|
slot: AgentSlot,
|
|
@@ -571,6 +875,10 @@ class AgentDisplayTransport:
|
|
|
571
875
|
True, DisplayTransportKind.NESTED, reason, fell_back=True)
|
|
572
876
|
return outcome
|
|
573
877
|
|
|
878
|
+
optimized = self._fast_switch_swap(slot, agent_tmux_name)
|
|
879
|
+
if optimized is not None:
|
|
880
|
+
return optimized
|
|
881
|
+
|
|
574
882
|
if (slot.swap_state is not None
|
|
575
883
|
and slot.swap_state.agent_tmux_name == agent_tmux_name
|
|
576
884
|
and slot.display_parked):
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Render a transcript completely before presenting its final page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from contextlib import redirect_stdout
|
|
10
|
+
|
|
11
|
+
from railmux import transcript
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: list[str] | None = None) -> int:
|
|
15
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
16
|
+
mouse = False
|
|
17
|
+
if "--mouse" in arguments:
|
|
18
|
+
arguments.remove("--mouse")
|
|
19
|
+
mouse = True
|
|
20
|
+
|
|
21
|
+
# TemporaryFile is unnamed (or unlinked immediately) on the managed POSIX
|
|
22
|
+
# runtimes. The formatted derivative is seekable for less +G, never
|
|
23
|
+
# becomes a reusable transcript cache, and disappears when this process
|
|
24
|
+
# exits. Provider JSONL files remain read-only.
|
|
25
|
+
with tempfile.TemporaryFile(
|
|
26
|
+
mode="w+", encoding="utf-8", newline="",
|
|
27
|
+
) as rendered:
|
|
28
|
+
try:
|
|
29
|
+
with redirect_stdout(rendered):
|
|
30
|
+
transcript.main(["transcript", *arguments])
|
|
31
|
+
except SystemExit as exc:
|
|
32
|
+
return int(exc.code) if isinstance(exc.code, int) else 1
|
|
33
|
+
rendered.flush()
|
|
34
|
+
rendered.seek(0)
|
|
35
|
+
less_argv = ["less", "-R", "+G"]
|
|
36
|
+
if mouse:
|
|
37
|
+
less_argv.extend(("--mouse", "--wheel-lines=3"))
|
|
38
|
+
environ = os.environ.copy()
|
|
39
|
+
environ.update({
|
|
40
|
+
"LESSSECURE": "1",
|
|
41
|
+
"LESSHISTFILE": "-",
|
|
42
|
+
"LESSOPEN": "",
|
|
43
|
+
"LESSCLOSE": "",
|
|
44
|
+
})
|
|
45
|
+
try:
|
|
46
|
+
return subprocess.call(less_argv, stdin=rendered, env=environ)
|
|
47
|
+
except OSError as exc:
|
|
48
|
+
print(f"Could not start read-only pager: {exc}", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
raise SystemExit(main())
|