railmux 0.4.1.dev6__tar.gz → 0.4.2.dev1__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.1.dev6/src/railmux.egg-info → railmux-0.4.2.dev1}/PKG-INFO +20 -6
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/README.md +19 -5
- railmux-0.4.2.dev1/src/railmux/__init__.py +1 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/fast_display_client.py +233 -10
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/fast_display_history.py +2 -2
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/fast_display_protocol.py +220 -8
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/fast_display_server.py +403 -11
- railmux-0.4.2.dev1/src/railmux/image_paste.py +448 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_attach_relay.py +102 -6
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1/src/railmux.egg-info}/PKG-INFO +20 -6
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fast_display_protocol.py +99 -1
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fast_display_server.py +150 -3
- railmux-0.4.2.dev1/tests/test_image_paste.py +74 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_attach_relay.py +143 -1
- railmux-0.4.1.dev6/src/railmux/__init__.py +0 -1
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/LICENSE +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/MANIFEST.in +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/pyproject.toml +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/setup.cfg +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/__main__.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/background_index.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/cli.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/config.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/discovery.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/favorites.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/launcher.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/local_open.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/models.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/modes.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/release_version.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/renames.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/self_update.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/session_index.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/settings.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/terminal_screen.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/transcript.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/app_test_harness.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_app_codex_placeholder.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_app_quit.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_background_index.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_cli.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_code_map.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_codex_index.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_config.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_config_cli.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_discovery.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_display_transport.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fast_display_selection.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_help_agent.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_keymap.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_launcher.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_local_open.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_modals.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_modes.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_new_project.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_options.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_path_codec.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_release_notes.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_release_version.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_remote_config.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_renames.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_restart_state.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_running_pane.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_running_sort.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_self_update.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_session_cache.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_session_index.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_session_lease.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_settings.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_sidebar.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_status.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_statusbar.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_system_deps.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_transcript.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_widgets.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tools/release_notes.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.1.dev6 → railmux-0.4.2.dev1}/tools/windows_archive_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2.dev1
|
|
4
4
|
Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
|
|
5
5
|
Author-email: Rightglow <zhang.taian@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -662,7 +662,21 @@ Selection → "Applications in terminal may access clipboard"*.)
|
|
|
662
662
|
> `Ctrl-B z` also toggles fullscreen (built into tmux) but zooms whichever
|
|
663
663
|
> pane has focus — it may fullscreen the sidebar instead of the agent.
|
|
664
664
|
|
|
665
|
-
### 2.
|
|
665
|
+
### 2. How do I paste an image into Codex or Claude Code?
|
|
666
|
+
|
|
667
|
+
With native Windows or `railmux ssh`, copy an image, focus the agent input,
|
|
668
|
+
then press **Ctrl+V**. If the terminal reserves Ctrl+V for text paste, use
|
|
669
|
+
**Alt+V**. Railmux transfers a bounded PNG to private storage visible to that
|
|
670
|
+
agent and pastes it into the current input without pressing Enter. Codex shows
|
|
671
|
+
its normal image attachment; Claude Code receives the image path it supports.
|
|
672
|
+
|
|
673
|
+
The shortcut is intercepted only when the local clipboard actually contains
|
|
674
|
+
an image and the current agent pane is still verified. Otherwise the original
|
|
675
|
+
key reaches Codex or Claude unchanged. macOS/Linux clipboard capture uses the
|
|
676
|
+
available native image reader (`osascript`/`pngpaste`, `wl-paste`, or `xclip`);
|
|
677
|
+
ordinary local macOS/Linux Railmux keeps the provider's native paste behavior.
|
|
678
|
+
|
|
679
|
+
### 3. Mouse buttons or F8/F9 don't work — what's wrong?
|
|
666
680
|
|
|
667
681
|
These are usually terminal‑side settings, not tmux or railmux.
|
|
668
682
|
|
|
@@ -710,7 +724,7 @@ has its own shortcut or key-mapping editor, remove the conflicting mapping or
|
|
|
710
724
|
configure it to send the corresponding F8/F9 function-key sequence to the
|
|
711
725
|
terminal session.
|
|
712
726
|
|
|
713
|
-
###
|
|
727
|
+
### 4. Using railmux over SSH
|
|
714
728
|
|
|
715
729
|
#### Choose a connection mode
|
|
716
730
|
|
|
@@ -856,7 +870,7 @@ do not kill tmux, the shared workspace, or its agents. See
|
|
|
856
870
|
[the architecture document](https://github.com/Rightglow/Railmux/blob/main/docs/ARCHITECTURE.md) for protocol, lease,
|
|
857
871
|
history-cache, and failure-containment details.
|
|
858
872
|
|
|
859
|
-
###
|
|
873
|
+
### 5. Will automated review sessions pollute my session list?
|
|
860
874
|
|
|
861
875
|
**Codex**: sessions created by `codex exec` (headless automation, pre‑commit
|
|
862
876
|
hooks, CI) are filtered automatically — railmux only shows interactive
|
|
@@ -872,7 +886,7 @@ claude -p --no-session-persistence "review this diff"
|
|
|
872
886
|
|
|
873
887
|
(`--no-session-persistence` is a Claude Code print-mode option.)
|
|
874
888
|
|
|
875
|
-
###
|
|
889
|
+
### 6. pip reports "externally-managed-environment"
|
|
876
890
|
|
|
877
891
|
Create a virtual environment, then install Railmux with that environment's
|
|
878
892
|
`pip`. This works on macOS, Linux, and WSL without modifying the system Python:
|
|
@@ -886,7 +900,7 @@ pip install railmux
|
|
|
886
900
|
`pipx install railmux` is an optional convenience for a globally available CLI;
|
|
887
901
|
it is not required on macOS or any other platform.
|
|
888
902
|
|
|
889
|
-
###
|
|
903
|
+
### 7. Can I open Railmux in multiple terminal windows?
|
|
890
904
|
|
|
891
905
|
Yes, as multiple views of one shared workspace. Normal launches and current
|
|
892
906
|
`railmux ssh` helpers may attach to the same managed `railmux` tmux session.
|
|
@@ -619,7 +619,21 @@ Selection → "Applications in terminal may access clipboard"*.)
|
|
|
619
619
|
> `Ctrl-B z` also toggles fullscreen (built into tmux) but zooms whichever
|
|
620
620
|
> pane has focus — it may fullscreen the sidebar instead of the agent.
|
|
621
621
|
|
|
622
|
-
### 2.
|
|
622
|
+
### 2. How do I paste an image into Codex or Claude Code?
|
|
623
|
+
|
|
624
|
+
With native Windows or `railmux ssh`, copy an image, focus the agent input,
|
|
625
|
+
then press **Ctrl+V**. If the terminal reserves Ctrl+V for text paste, use
|
|
626
|
+
**Alt+V**. Railmux transfers a bounded PNG to private storage visible to that
|
|
627
|
+
agent and pastes it into the current input without pressing Enter. Codex shows
|
|
628
|
+
its normal image attachment; Claude Code receives the image path it supports.
|
|
629
|
+
|
|
630
|
+
The shortcut is intercepted only when the local clipboard actually contains
|
|
631
|
+
an image and the current agent pane is still verified. Otherwise the original
|
|
632
|
+
key reaches Codex or Claude unchanged. macOS/Linux clipboard capture uses the
|
|
633
|
+
available native image reader (`osascript`/`pngpaste`, `wl-paste`, or `xclip`);
|
|
634
|
+
ordinary local macOS/Linux Railmux keeps the provider's native paste behavior.
|
|
635
|
+
|
|
636
|
+
### 3. Mouse buttons or F8/F9 don't work — what's wrong?
|
|
623
637
|
|
|
624
638
|
These are usually terminal‑side settings, not tmux or railmux.
|
|
625
639
|
|
|
@@ -667,7 +681,7 @@ has its own shortcut or key-mapping editor, remove the conflicting mapping or
|
|
|
667
681
|
configure it to send the corresponding F8/F9 function-key sequence to the
|
|
668
682
|
terminal session.
|
|
669
683
|
|
|
670
|
-
###
|
|
684
|
+
### 4. Using railmux over SSH
|
|
671
685
|
|
|
672
686
|
#### Choose a connection mode
|
|
673
687
|
|
|
@@ -813,7 +827,7 @@ do not kill tmux, the shared workspace, or its agents. See
|
|
|
813
827
|
[the architecture document](https://github.com/Rightglow/Railmux/blob/main/docs/ARCHITECTURE.md) for protocol, lease,
|
|
814
828
|
history-cache, and failure-containment details.
|
|
815
829
|
|
|
816
|
-
###
|
|
830
|
+
### 5. Will automated review sessions pollute my session list?
|
|
817
831
|
|
|
818
832
|
**Codex**: sessions created by `codex exec` (headless automation, pre‑commit
|
|
819
833
|
hooks, CI) are filtered automatically — railmux only shows interactive
|
|
@@ -829,7 +843,7 @@ claude -p --no-session-persistence "review this diff"
|
|
|
829
843
|
|
|
830
844
|
(`--no-session-persistence` is a Claude Code print-mode option.)
|
|
831
845
|
|
|
832
|
-
###
|
|
846
|
+
### 6. pip reports "externally-managed-environment"
|
|
833
847
|
|
|
834
848
|
Create a virtual environment, then install Railmux with that environment's
|
|
835
849
|
`pip`. This works on macOS, Linux, and WSL without modifying the system Python:
|
|
@@ -843,7 +857,7 @@ pip install railmux
|
|
|
843
857
|
`pipx install railmux` is an optional convenience for a globally available CLI;
|
|
844
858
|
it is not required on macOS or any other platform.
|
|
845
859
|
|
|
846
|
-
###
|
|
860
|
+
### 7. Can I open Railmux in multiple terminal windows?
|
|
847
861
|
|
|
848
862
|
Yes, as multiple views of one shared workspace. Normal launches and current
|
|
849
863
|
`railmux ssh` helpers may attach to the same managed `railmux` tmux session.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.2.dev1"
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import base64
|
|
14
|
+
import errno
|
|
14
15
|
import os
|
|
15
16
|
import re
|
|
16
17
|
import select
|
|
@@ -22,12 +23,13 @@ import sys
|
|
|
22
23
|
import termios
|
|
23
24
|
import time
|
|
24
25
|
import tty
|
|
26
|
+
from collections import deque
|
|
25
27
|
from collections.abc import Callable, Mapping
|
|
26
28
|
from dataclasses import dataclass
|
|
27
29
|
from enum import Enum
|
|
28
30
|
from typing import BinaryIO, Optional, Sequence
|
|
29
31
|
|
|
30
|
-
from railmux import __version__, local_clipboard, local_open
|
|
32
|
+
from railmux import __version__, image_paste, local_clipboard, local_open
|
|
31
33
|
from railmux.config import (
|
|
32
34
|
ConfigError,
|
|
33
35
|
load_config,
|
|
@@ -59,6 +61,8 @@ from railmux.fast_display_protocol import (
|
|
|
59
61
|
ClaudeHistoryPolicyResult,
|
|
60
62
|
HistoryBatch,
|
|
61
63
|
HistorySnapshot,
|
|
64
|
+
ImagePasteResult,
|
|
65
|
+
MAX_IMAGE_CHUNK_BYTES,
|
|
62
66
|
PathKind,
|
|
63
67
|
PathOpenResult,
|
|
64
68
|
PathResult,
|
|
@@ -74,6 +78,9 @@ from railmux.fast_display_protocol import (
|
|
|
74
78
|
encode_heartbeat,
|
|
75
79
|
encode_claude_history_policy,
|
|
76
80
|
encode_input,
|
|
81
|
+
encode_image_begin,
|
|
82
|
+
encode_image_chunk,
|
|
83
|
+
encode_image_commit,
|
|
77
84
|
encode_keyframe_request,
|
|
78
85
|
encode_path_request,
|
|
79
86
|
encode_path_open_request,
|
|
@@ -120,9 +127,108 @@ _WINDOWS_HISTORY_WHEEL_LINES = 3
|
|
|
120
127
|
_SELECTION_HIGHLIGHT_SECONDS = 2.0
|
|
121
128
|
_CLICK_FLASH_SECONDS = 0.18
|
|
122
129
|
_PATH_OPEN_TIMEOUT = 5.0
|
|
130
|
+
_IMAGE_WRITER_LOW_WATER = 96 * 1024
|
|
131
|
+
_PROTOCOL_WRITER_MAX_BYTES = 512 * 1024
|
|
132
|
+
_IMAGE_BUFFERED_INPUT_LIMIT = 64 * 1024
|
|
123
133
|
_TERMUX_TOUCH_HINT = "Tap the prompt again to open the keyboard"
|
|
124
134
|
_REMOTE_ATTACH_TIMEOUT = 30.0
|
|
125
135
|
_REMOTE_INSTALL_TIMEOUT = 300.0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _OutboundProtocolWriter:
|
|
139
|
+
"""Nonblocking ordered writes so image upload cannot stall rendering."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, stream: BinaryIO) -> None:
|
|
142
|
+
self._queue: deque[bytes] = deque()
|
|
143
|
+
self._offset = 0
|
|
144
|
+
self._pending_bytes = 0
|
|
145
|
+
self._fd = -1
|
|
146
|
+
self.failed = False
|
|
147
|
+
self.replace(stream)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def pending_bytes(self) -> int:
|
|
151
|
+
return self._pending_bytes
|
|
152
|
+
|
|
153
|
+
def replace(self, stream: BinaryIO) -> None:
|
|
154
|
+
self._queue.clear()
|
|
155
|
+
self._offset = 0
|
|
156
|
+
self._pending_bytes = 0
|
|
157
|
+
self._fd = stream.fileno()
|
|
158
|
+
self.failed = False
|
|
159
|
+
os.set_blocking(self._fd, False)
|
|
160
|
+
|
|
161
|
+
def enqueue(self, frame: bytes) -> None:
|
|
162
|
+
if not frame or self.failed:
|
|
163
|
+
return
|
|
164
|
+
if self._pending_bytes + len(frame) > _PROTOCOL_WRITER_MAX_BYTES:
|
|
165
|
+
self.failed = True
|
|
166
|
+
return
|
|
167
|
+
self._queue.append(frame)
|
|
168
|
+
self._pending_bytes += len(frame)
|
|
169
|
+
|
|
170
|
+
def flush(self, budget: int = 256 * 1024) -> None:
|
|
171
|
+
written_total = 0
|
|
172
|
+
while self._queue and written_total < budget and not self.failed:
|
|
173
|
+
frame = self._queue[0]
|
|
174
|
+
try:
|
|
175
|
+
written = os.write(self._fd, frame[self._offset :])
|
|
176
|
+
except BlockingIOError:
|
|
177
|
+
return
|
|
178
|
+
except OSError as exc:
|
|
179
|
+
if exc.errno in (errno.EPIPE, errno.EBADF):
|
|
180
|
+
self.failed = True
|
|
181
|
+
return
|
|
182
|
+
raise
|
|
183
|
+
if written <= 0:
|
|
184
|
+
return
|
|
185
|
+
written_total += written
|
|
186
|
+
self._pending_bytes -= written
|
|
187
|
+
self._offset += written
|
|
188
|
+
if self._offset == len(frame):
|
|
189
|
+
self._queue.popleft()
|
|
190
|
+
self._offset = 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class _ClientImageUpload:
|
|
195
|
+
request_id: int
|
|
196
|
+
pane_id: str
|
|
197
|
+
generation: int
|
|
198
|
+
image: image_paste.ClipboardImage
|
|
199
|
+
offset: int = 0
|
|
200
|
+
begun: bool = False
|
|
201
|
+
committed: bool = False
|
|
202
|
+
|
|
203
|
+
def pump(self, writer: _OutboundProtocolWriter) -> None:
|
|
204
|
+
if self.committed:
|
|
205
|
+
return
|
|
206
|
+
if not self.begun:
|
|
207
|
+
writer.enqueue(
|
|
208
|
+
encode_image_begin(
|
|
209
|
+
self.request_id,
|
|
210
|
+
self.pane_id,
|
|
211
|
+
self.generation,
|
|
212
|
+
len(self.image.data),
|
|
213
|
+
self.image.digest,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
self.begun = True
|
|
217
|
+
while (
|
|
218
|
+
self.offset < len(self.image.data)
|
|
219
|
+
and writer.pending_bytes < _IMAGE_WRITER_LOW_WATER
|
|
220
|
+
):
|
|
221
|
+
chunk = self.image.data[
|
|
222
|
+
self.offset : self.offset + MAX_IMAGE_CHUNK_BYTES
|
|
223
|
+
]
|
|
224
|
+
writer.enqueue(encode_image_chunk(self.request_id, self.offset, chunk))
|
|
225
|
+
self.offset += len(chunk)
|
|
226
|
+
if (
|
|
227
|
+
self.offset == len(self.image.data)
|
|
228
|
+
and writer.pending_bytes < _IMAGE_WRITER_LOW_WATER
|
|
229
|
+
):
|
|
230
|
+
writer.enqueue(encode_image_commit(self.request_id))
|
|
231
|
+
self.committed = True
|
|
126
232
|
_FIRST_FRAME_TIMEOUT = 30.0
|
|
127
233
|
_REMOTE_ATTACH_RETRY_DELAY = 0.2
|
|
128
234
|
_HEARTBEAT_INTERVAL = 5.0
|
|
@@ -1031,7 +1137,7 @@ class TerminalSurface:
|
|
|
1031
1137
|
self,
|
|
1032
1138
|
requested: TerminalMode,
|
|
1033
1139
|
) -> TerminalMode:
|
|
1034
|
-
"""Mirror only input-affecting modes explicitly carried by protocol
|
|
1140
|
+
"""Mirror only input-affecting modes explicitly carried by protocol v17."""
|
|
1035
1141
|
disabled = self.terminal_modes & ~requested
|
|
1036
1142
|
enabled = requested & ~self.terminal_modes
|
|
1037
1143
|
controls: list[bytes] = []
|
|
@@ -1935,7 +2041,7 @@ def _finish_remote_attach(
|
|
|
1935
2041
|
raise ProbeError("remote display helper failed before attaching")
|
|
1936
2042
|
|
|
1937
2043
|
_stop_unstarted_remote(process)
|
|
1938
|
-
# A current
|
|
2044
|
+
# A current v17 helper holds the mutex only while registering its exact tmux
|
|
1939
2045
|
# child. Give that ordinary race one fresh SSH process before presenting
|
|
1940
2046
|
# the explicit legacy-lock takeover choice.
|
|
1941
2047
|
time.sleep(_REMOTE_ATTACH_RETRY_DELAY)
|
|
@@ -2442,17 +2548,84 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2442
2548
|
path_open_prompt_mouse_button: int | None = None
|
|
2443
2549
|
local_status_mouse_button: int | None = None
|
|
2444
2550
|
periodic_prefetch = PeriodicPrefetchGate()
|
|
2551
|
+
protocol_writer = _OutboundProtocolWriter(process.stdin)
|
|
2552
|
+
image_request_id = 0
|
|
2553
|
+
image_upload: _ClientImageUpload | None = None
|
|
2554
|
+
image_buffered_input: list[bytes] = []
|
|
2555
|
+
image_buffered_bytes = 0
|
|
2445
2556
|
|
|
2446
2557
|
def send_protocol_frame(frame: bytes) -> None:
|
|
2447
2558
|
nonlocal remote_closed
|
|
2448
2559
|
if remote_closed:
|
|
2449
2560
|
return
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
process.stdin.flush()
|
|
2453
|
-
except BrokenPipeError:
|
|
2561
|
+
protocol_writer.enqueue(frame)
|
|
2562
|
+
if protocol_writer.failed:
|
|
2454
2563
|
remote_closed = True
|
|
2455
2564
|
|
|
2565
|
+
def begin_image_paste() -> bool:
|
|
2566
|
+
nonlocal image_request_id, image_upload, history_info_until
|
|
2567
|
+
if image_upload is not None:
|
|
2568
|
+
surface.show_local_status(
|
|
2569
|
+
"Another image paste is still uploading",
|
|
2570
|
+
level="warning",
|
|
2571
|
+
)
|
|
2572
|
+
history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
|
|
2573
|
+
return True
|
|
2574
|
+
if latest_screen is None:
|
|
2575
|
+
return False
|
|
2576
|
+
pane_id = history.pane_id_at_position(
|
|
2577
|
+
latest_screen.cursor_x,
|
|
2578
|
+
latest_screen.cursor_y,
|
|
2579
|
+
)
|
|
2580
|
+
route = next(
|
|
2581
|
+
(
|
|
2582
|
+
candidate
|
|
2583
|
+
for candidate in history.visible_routes
|
|
2584
|
+
if candidate.pane_id == pane_id
|
|
2585
|
+
),
|
|
2586
|
+
None,
|
|
2587
|
+
)
|
|
2588
|
+
if route is None or route.route_generation == 0:
|
|
2589
|
+
return False
|
|
2590
|
+
try:
|
|
2591
|
+
image = image_paste.read_clipboard_png()
|
|
2592
|
+
except image_paste.ImagePasteError as exc:
|
|
2593
|
+
surface.show_local_status(str(exc), level="warning")
|
|
2594
|
+
history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
|
|
2595
|
+
return False
|
|
2596
|
+
if image is None:
|
|
2597
|
+
return False
|
|
2598
|
+
image_request_id = (image_request_id + 1) & 0xFFFFFFFF
|
|
2599
|
+
if image_request_id == 0:
|
|
2600
|
+
image_request_id = 1
|
|
2601
|
+
image_upload = _ClientImageUpload(
|
|
2602
|
+
image_request_id,
|
|
2603
|
+
route.pane_id,
|
|
2604
|
+
route.route_generation,
|
|
2605
|
+
image,
|
|
2606
|
+
)
|
|
2607
|
+
size_mib = len(image.data) / (1024 * 1024)
|
|
2608
|
+
surface.show_local_status(
|
|
2609
|
+
f"Uploading {image.width}×{image.height} image ({size_mib:.1f} MiB)…"
|
|
2610
|
+
)
|
|
2611
|
+
history_info_until = None
|
|
2612
|
+
return True
|
|
2613
|
+
|
|
2614
|
+
def buffer_provider_input(data: bytes) -> bool:
|
|
2615
|
+
nonlocal image_buffered_bytes, history_info_until
|
|
2616
|
+
if image_upload is None:
|
|
2617
|
+
return False
|
|
2618
|
+
if image_buffered_bytes + len(data) > _IMAGE_BUFFERED_INPUT_LIMIT:
|
|
2619
|
+
surface.show_local_status(
|
|
2620
|
+
"Image upload is still finishing; additional input was not sent",
|
|
2621
|
+
level="warning",
|
|
2622
|
+
)
|
|
2623
|
+
history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
|
|
2624
|
+
return True
|
|
2625
|
+
image_buffered_input.append(data)
|
|
2626
|
+
image_buffered_bytes += len(data)
|
|
2627
|
+
return True
|
|
2628
|
+
|
|
2456
2629
|
def show_open_result(result: local_open.OpenResult) -> None:
|
|
2457
2630
|
nonlocal history_info_until
|
|
2458
2631
|
if result.copy_data is not None:
|
|
@@ -2881,6 +3054,16 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2881
3054
|
return
|
|
2882
3055
|
if not part:
|
|
2883
3056
|
return
|
|
3057
|
+
if not opaque_paste and isinstance(part, bytes):
|
|
3058
|
+
image_parts = image_paste.split_shortcuts(part)
|
|
3059
|
+
if len(image_parts) > 1:
|
|
3060
|
+
for image_part in image_parts:
|
|
3061
|
+
handle_terminal_part(image_part, input_batch)
|
|
3062
|
+
return
|
|
3063
|
+
if part in image_paste.IMAGE_PASTE_KEYS and begin_image_paste():
|
|
3064
|
+
restore = history.cancel()
|
|
3065
|
+
apply_history_action(HistoryAction(restore_live=restore))
|
|
3066
|
+
return
|
|
2884
3067
|
if not opaque_paste and touch_keyboard.consumes_focus_out(part):
|
|
2885
3068
|
# Opening Android's input view can briefly remove focus from the
|
|
2886
3069
|
# terminal View even though the user is still interacting with
|
|
@@ -2965,7 +3148,8 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2965
3148
|
# types. Reassert only the cached cursor on Termux focus-in; the
|
|
2966
3149
|
# remote focus event remains authoritative and is still forwarded.
|
|
2967
3150
|
surface.reassert_cursor()
|
|
2968
|
-
|
|
3151
|
+
if not buffer_provider_input(part):
|
|
3152
|
+
send_protocol_frame(encode_input(part))
|
|
2969
3153
|
|
|
2970
3154
|
surface.show_local_status(
|
|
2971
3155
|
"Ctrl-] disconnects · Ctrl-B d detaches · "
|
|
@@ -2982,6 +3166,11 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2982
3166
|
) as windows_interrupts,
|
|
2983
3167
|
):
|
|
2984
3168
|
while True:
|
|
3169
|
+
if image_upload is not None:
|
|
3170
|
+
image_upload.pump(protocol_writer)
|
|
3171
|
+
protocol_writer.flush()
|
|
3172
|
+
if protocol_writer.failed:
|
|
3173
|
+
remote_closed = True
|
|
2985
3174
|
pending_interrupts = windows_interrupts.consume()
|
|
2986
3175
|
if pending_interrupts:
|
|
2987
3176
|
input_batch = _LocalInputBatch(history_paint)
|
|
@@ -3166,6 +3355,25 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3166
3355
|
time.monotonic() + _HISTORY_INFO_SECONDS
|
|
3167
3356
|
)
|
|
3168
3357
|
continue
|
|
3358
|
+
if isinstance(message, ImagePasteResult):
|
|
3359
|
+
if (
|
|
3360
|
+
image_upload is None
|
|
3361
|
+
or message.request_id != image_upload.request_id
|
|
3362
|
+
):
|
|
3363
|
+
continue
|
|
3364
|
+
image_upload = None
|
|
3365
|
+
surface.show_local_status(
|
|
3366
|
+
message.message,
|
|
3367
|
+
level=message.level,
|
|
3368
|
+
)
|
|
3369
|
+
history_info_until = (
|
|
3370
|
+
time.monotonic() + _HISTORY_INFO_SECONDS
|
|
3371
|
+
)
|
|
3372
|
+
for buffered in image_buffered_input:
|
|
3373
|
+
send_protocol_frame(encode_input(buffered))
|
|
3374
|
+
image_buffered_input.clear()
|
|
3375
|
+
image_buffered_bytes = 0
|
|
3376
|
+
continue
|
|
3169
3377
|
if isinstance(message, HistoryBatch):
|
|
3170
3378
|
accepted_prefetch_id = history.prefetch_pending_id
|
|
3171
3379
|
action = history.accept_prefetch(message)
|
|
@@ -3320,6 +3528,11 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3320
3528
|
now=time.monotonic(),
|
|
3321
3529
|
)
|
|
3322
3530
|
now = time.monotonic()
|
|
3531
|
+
if image_upload is not None:
|
|
3532
|
+
image_upload.pump(protocol_writer)
|
|
3533
|
+
protocol_writer.flush()
|
|
3534
|
+
if protocol_writer.failed:
|
|
3535
|
+
remote_closed = True
|
|
3323
3536
|
history_paint.flush(
|
|
3324
3537
|
surface,
|
|
3325
3538
|
latest_screen,
|
|
@@ -3430,7 +3643,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3430
3643
|
history_info_until = now + _HISTORY_INFO_SECONDS
|
|
3431
3644
|
if local_exit:
|
|
3432
3645
|
break
|
|
3433
|
-
connection_ended = remote_closed or (
|
|
3646
|
+
connection_ended = remote_closed or protocol_writer.failed or (
|
|
3434
3647
|
process.poll() is not None and not events
|
|
3435
3648
|
)
|
|
3436
3649
|
if connection_ended:
|
|
@@ -3467,6 +3680,11 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3467
3680
|
selectors.EVENT_READ,
|
|
3468
3681
|
"remote",
|
|
3469
3682
|
)
|
|
3683
|
+
image_was_cancelled = image_upload is not None
|
|
3684
|
+
image_upload = None
|
|
3685
|
+
image_buffered_input.clear()
|
|
3686
|
+
image_buffered_bytes = 0
|
|
3687
|
+
protocol_writer.replace(process.stdin)
|
|
3470
3688
|
decoder = ServerMessageDecoder()
|
|
3471
3689
|
model = ScreenModel()
|
|
3472
3690
|
terminal_input = TerminalInputDecoder()
|
|
@@ -3510,7 +3728,12 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3510
3728
|
reconnect_metrics["successes"] += 1
|
|
3511
3729
|
periodic_prefetch.reset()
|
|
3512
3730
|
surface.show_local_status(
|
|
3513
|
-
|
|
3731
|
+
(
|
|
3732
|
+
"Reconnected; image paste was cancelled safely"
|
|
3733
|
+
if image_was_cancelled
|
|
3734
|
+
else "Reconnected; waiting for a fresh screen"
|
|
3735
|
+
),
|
|
3736
|
+
level="warning" if image_was_cancelled else "info",
|
|
3514
3737
|
)
|
|
3515
3738
|
continue
|
|
3516
3739
|
break
|
|
@@ -1199,7 +1199,7 @@ class LocalHistoryView:
|
|
|
1199
1199
|
# preserves the strongest ambiguity check when the live pane did not
|
|
1200
1200
|
# change while the deeper capture was in flight.
|
|
1201
1201
|
# KMP keeps this compatibility path linear even for long repeated
|
|
1202
|
-
# captures. Protocol-
|
|
1202
|
+
# captures. Protocol-v17 coordinates normally bypass it, but bounded
|
|
1203
1203
|
# transcript tails and older peers still require exact content proof.
|
|
1204
1204
|
prefix = [0] * len(anchor)
|
|
1205
1205
|
matched = 0
|
|
@@ -1273,7 +1273,7 @@ class LocalHistoryView:
|
|
|
1273
1273
|
incoming: HistorySnapshot,
|
|
1274
1274
|
viewport: _HistoryViewport,
|
|
1275
1275
|
) -> int | None:
|
|
1276
|
-
"""Map a frozen viewport through
|
|
1276
|
+
"""Map a frozen viewport through v17 absolute timeline coordinates."""
|
|
1277
1277
|
previous = viewport.snapshot
|
|
1278
1278
|
if not cls._has_timeline(previous) or not cls._has_timeline(incoming):
|
|
1279
1279
|
return None
|