railmux 0.4.0.dev15__tar.gz → 0.4.0.dev17__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.dev15/src/railmux.egg-info → railmux-0.4.0.dev17}/PKG-INFO +29 -12
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/README.md +28 -11
- railmux-0.4.0.dev17/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/fast_display_client.py +142 -40
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/fast_display_history.py +11 -9
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/fast_display_server.py +17 -3
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/models.py +3 -0
- railmux-0.4.0.dev17/src/railmux/session_lease.py +554 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/tmux_binding_manager.py +5 -2
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/tmux_ctl.py +34 -59
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/tmux_server.py +15 -1
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/app.py +639 -128
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/running_pane.py +4 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/sessions_pane.py +10 -3
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17/src/railmux.egg-info}/PKG-INFO +29 -12
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_cli.py +3 -1
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_codex_delete.py +4 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_codex_lineage.py +49 -1
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_fast_full_window.py +184 -14
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_focus_highlight.py +8 -14
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_fullscreen_binding.py +206 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_orphan_recovery.py +40 -1
- railmux-0.4.0.dev17/tests/test_session_lease.py +491 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_sessions_pane.py +15 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_soft_quit.py +10 -5
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_terminal_size.py +12 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tmux_binding_manager.py +25 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tmux_ctl.py +24 -10
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tmux_server.py +3 -1
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tmux_status.py +57 -8
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_attach_relay.py +2 -2
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_msys2.py +2 -1
- railmux-0.4.0.dev15/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/LICENSE +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/pyproject.toml +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/setup.cfg +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev15 → railmux-0.4.0.dev17}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev17
|
|
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
|
|
@@ -170,7 +170,7 @@ for focus and layout limits.
|
|
|
170
170
|
|--------|--------|
|
|
171
171
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
172
172
|
| Left-click (running) | Switch the Target pane to that session |
|
|
173
|
-
| Wheel
|
|
173
|
+
| Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
|
|
174
174
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
175
175
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
176
176
|
|
|
@@ -373,16 +373,21 @@ sessions move ahead of the other results.
|
|
|
373
373
|
|
|
374
374
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
375
375
|
the Target pane without starting or resuming the agent. For a running session,
|
|
376
|
-
press
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
history
|
|
376
|
+
press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
|
|
377
|
+
or change the session. User and assistant messages, tool calls, and abbreviated
|
|
378
|
+
tool output are colour-coded, while internal context and encrypted reasoning
|
|
379
|
+
are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
|
|
380
|
+
projected onto the provider's current conversation: the retained prefix and
|
|
381
|
+
replacement suffix remain visible, while the abandoned suffix is hidden.
|
|
382
|
+
|
|
383
|
+
Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
384
|
+
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
385
|
+
per-pane history and normally preserves the live terminal's captured styling.
|
|
386
|
+
When Railmux confirms that a running Codex session advanced after rewind or
|
|
387
|
+
steering, it resets only the current terminal view before Codex redraws and
|
|
388
|
+
invalidates that pane's SSH history generation. Retained tmux scrollback is not
|
|
389
|
+
erased; the SSH history for that branched generation uses the canonical
|
|
390
|
+
rollout so its abandoned suffix stays hidden.
|
|
386
391
|
|
|
387
392
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
388
393
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -423,6 +428,18 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
|
|
|
423
428
|
Railmux, so existing conversations appear in the sidebar and resume the same
|
|
424
429
|
way. Non-resumable one-off runs such as `codex exec` are filtered out.
|
|
425
430
|
|
|
431
|
+
If the same Claude Code or Codex history root is shared across hosts, Railmux
|
|
432
|
+
uses a provider-session lease to prevent two machines from resuming the same
|
|
433
|
+
conversation concurrently. A remotely owned conversation remains in Sessions
|
|
434
|
+
as **running on HOST**, but is not presented as a locally attachable Running
|
|
435
|
+
entry; resume fails safely until that host's exact provider pane exits. The
|
|
436
|
+
lease follows detached agents across Soft Quit and covers every known Codex
|
|
437
|
+
rewind-lineage ID at resume; later rewinds retain a stable locked lineage
|
|
438
|
+
anchor. Delete also checks the shared lease immediately before
|
|
439
|
+
starting and refuses remotely owned history. The shared filesystem must provide
|
|
440
|
+
cross-host POSIX advisory-lock semantics; mounts configured with local-only or
|
|
441
|
+
disabled locking cannot provide this guarantee.
|
|
442
|
+
|
|
426
443
|
### Restarts and quitting
|
|
427
444
|
|
|
428
445
|
Each opened agent runs in a detached tmux session, so switching sessions does
|
|
@@ -127,7 +127,7 @@ for focus and layout limits.
|
|
|
127
127
|
|--------|--------|
|
|
128
128
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
129
129
|
| Left-click (running) | Switch the Target pane to that session |
|
|
130
|
-
| Wheel
|
|
130
|
+
| Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
|
|
131
131
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
132
132
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
133
133
|
|
|
@@ -330,16 +330,21 @@ sessions move ahead of the other results.
|
|
|
330
330
|
|
|
331
331
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
332
332
|
the Target pane without starting or resuming the agent. For a running session,
|
|
333
|
-
press
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
history
|
|
333
|
+
press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
|
|
334
|
+
or change the session. User and assistant messages, tool calls, and abbreviated
|
|
335
|
+
tool output are colour-coded, while internal context and encrypted reasoning
|
|
336
|
+
are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
|
|
337
|
+
projected onto the provider's current conversation: the retained prefix and
|
|
338
|
+
replacement suffix remain visible, while the abandoned suffix is hidden.
|
|
339
|
+
|
|
340
|
+
Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
341
|
+
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
342
|
+
per-pane history and normally preserves the live terminal's captured styling.
|
|
343
|
+
When Railmux confirms that a running Codex session advanced after rewind or
|
|
344
|
+
steering, it resets only the current terminal view before Codex redraws and
|
|
345
|
+
invalidates that pane's SSH history generation. Retained tmux scrollback is not
|
|
346
|
+
erased; the SSH history for that branched generation uses the canonical
|
|
347
|
+
rollout so its abandoned suffix stays hidden.
|
|
343
348
|
|
|
344
349
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
345
350
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -380,6 +385,18 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
|
|
|
380
385
|
Railmux, so existing conversations appear in the sidebar and resume the same
|
|
381
386
|
way. Non-resumable one-off runs such as `codex exec` are filtered out.
|
|
382
387
|
|
|
388
|
+
If the same Claude Code or Codex history root is shared across hosts, Railmux
|
|
389
|
+
uses a provider-session lease to prevent two machines from resuming the same
|
|
390
|
+
conversation concurrently. A remotely owned conversation remains in Sessions
|
|
391
|
+
as **running on HOST**, but is not presented as a locally attachable Running
|
|
392
|
+
entry; resume fails safely until that host's exact provider pane exits. The
|
|
393
|
+
lease follows detached agents across Soft Quit and covers every known Codex
|
|
394
|
+
rewind-lineage ID at resume; later rewinds retain a stable locked lineage
|
|
395
|
+
anchor. Delete also checks the shared lease immediately before
|
|
396
|
+
starting and refuses remotely owned history. The shared filesystem must provide
|
|
397
|
+
cross-host POSIX advisory-lock semantics; mounts configured with local-only or
|
|
398
|
+
disabled locking cannot provide this guarantee.
|
|
399
|
+
|
|
383
400
|
### Restarts and quitting
|
|
384
401
|
|
|
385
402
|
Each opened agent runs in a detached tmux session, so switching sessions does
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev17"
|
|
@@ -202,6 +202,14 @@ def full_repaint(screen: AppliedScreen) -> AppliedScreen:
|
|
|
202
202
|
)
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def selection_changed_rows(
|
|
206
|
+
before: tuple[SelectionSegment, ...],
|
|
207
|
+
after: tuple[SelectionSegment, ...],
|
|
208
|
+
) -> set[int]:
|
|
209
|
+
"""Rows that must be restored when one local highlight changes."""
|
|
210
|
+
return {row for row, _column, _text in (*before, *after)}
|
|
211
|
+
|
|
212
|
+
|
|
205
213
|
def focus_in_frame_for_screen(screen: AppliedScreen | None) -> bytes | None:
|
|
206
214
|
"""Reassert focus only while the remote application requested events."""
|
|
207
215
|
if screen is None or not screen.terminal_modes & TerminalMode.FOCUS_EVENTS:
|
|
@@ -637,6 +645,10 @@ class TerminalSurface:
|
|
|
637
645
|
self.mouse_active = False
|
|
638
646
|
self.mouse_suspended = False
|
|
639
647
|
self.cursor_hidden = False
|
|
648
|
+
# Track the physical emulator cursor separately from the remote model.
|
|
649
|
+
# Re-sending DECSET/DECRST 25 on every 20 fps patch restarts the cursor
|
|
650
|
+
# animation in Windows Terminal and presents as a rapid flash.
|
|
651
|
+
self._painted_cursor_visible: bool | None = None
|
|
640
652
|
self.terminal_modes = TerminalMode.NONE
|
|
641
653
|
self.physical_size: os.terminal_size | None = None
|
|
642
654
|
self._last_screen: AppliedScreen | None = None
|
|
@@ -694,6 +706,7 @@ class TerminalSurface:
|
|
|
694
706
|
if interactive and not self.cursor_hidden:
|
|
695
707
|
controls.append(b"\033[?25l")
|
|
696
708
|
self.cursor_hidden = True
|
|
709
|
+
self._painted_cursor_visible = False
|
|
697
710
|
if (
|
|
698
711
|
interactive
|
|
699
712
|
and self.mouse
|
|
@@ -793,6 +806,7 @@ class TerminalSurface:
|
|
|
793
806
|
self.mouse_active = False
|
|
794
807
|
self.mouse_suspended = False
|
|
795
808
|
self.cursor_hidden = False
|
|
809
|
+
self._painted_cursor_visible = True
|
|
796
810
|
self.interaction_active = True
|
|
797
811
|
|
|
798
812
|
def show_local_status(
|
|
@@ -813,7 +827,8 @@ class TerminalSurface:
|
|
|
813
827
|
self._local_status_level = level
|
|
814
828
|
self._local_status_interruptible = interruptible
|
|
815
829
|
self._local_status_expires_at = expires_at
|
|
816
|
-
|
|
830
|
+
restore_cursor = self._last_screen is not None
|
|
831
|
+
rendered = [self._render_local_status(hide_cursor=not restore_cursor)]
|
|
817
832
|
if self._last_screen is not None:
|
|
818
833
|
projection_top, visible_height = self._projection(
|
|
819
834
|
self._last_screen.height
|
|
@@ -828,7 +843,7 @@ class TerminalSurface:
|
|
|
828
843
|
self.stream.write(b"".join(rendered))
|
|
829
844
|
self.stream.flush()
|
|
830
845
|
|
|
831
|
-
def _render_local_status(self) -> bytes:
|
|
846
|
+
def _render_local_status(self, *, hide_cursor: bool = True) -> bytes:
|
|
832
847
|
"""Render and locate the current local status-right replacement."""
|
|
833
848
|
if self._local_status_text is None:
|
|
834
849
|
self._local_status_bounds = None
|
|
@@ -841,10 +856,14 @@ class TerminalSurface:
|
|
|
841
856
|
if self.physical_size is not None:
|
|
842
857
|
safe = safe[: self.physical_size.columns]
|
|
843
858
|
self._local_status_bounds = (max(1, height), 1, max(1, len(safe)))
|
|
844
|
-
|
|
859
|
+
rendered = (
|
|
845
860
|
f"\033[?7l\033[0m\033[{max(1, height)};1H\033[2K{safe}"
|
|
846
|
-
"\033[0m\033[?7h
|
|
861
|
+
"\033[0m\033[?7h"
|
|
847
862
|
).encode("utf-8")
|
|
863
|
+
if hide_cursor:
|
|
864
|
+
rendered += b"\033[?25l"
|
|
865
|
+
self._painted_cursor_visible = False
|
|
866
|
+
return rendered
|
|
848
867
|
width = screen.width
|
|
849
868
|
if self.physical_size is not None:
|
|
850
869
|
width = min(width, self.physical_size.columns)
|
|
@@ -862,8 +881,8 @@ class TerminalSurface:
|
|
|
862
881
|
"warning": b"\033[1;38;5;220m",
|
|
863
882
|
"error": b"\033[1;38;5;196m",
|
|
864
883
|
}.get(level, b"\033[38;5;231m")
|
|
865
|
-
|
|
866
|
-
|
|
884
|
+
rendered = b"".join(
|
|
885
|
+
[
|
|
867
886
|
b"\033[?7l\033[0m",
|
|
868
887
|
f"\033[{max(1, height)};{reserved + 1}H".encode(),
|
|
869
888
|
background,
|
|
@@ -871,9 +890,13 @@ class TerminalSurface:
|
|
|
871
890
|
b"\033[K",
|
|
872
891
|
f"\033[{max(1, height)};{column}H".encode(),
|
|
873
892
|
safe.encode("utf-8"),
|
|
874
|
-
b"\033[0m
|
|
875
|
-
|
|
893
|
+
b"\033[0m",
|
|
894
|
+
]
|
|
876
895
|
)
|
|
896
|
+
if hide_cursor:
|
|
897
|
+
rendered += b"\033[?25l"
|
|
898
|
+
self._painted_cursor_visible = False
|
|
899
|
+
return rendered
|
|
877
900
|
|
|
878
901
|
def clear_local_status(self) -> None:
|
|
879
902
|
"""Forget local feedback before restoring the authoritative screen."""
|
|
@@ -991,6 +1014,7 @@ class TerminalSurface:
|
|
|
991
1014
|
)
|
|
992
1015
|
self.stream.write(b"".join(rendered))
|
|
993
1016
|
self.stream.flush()
|
|
1017
|
+
self._painted_cursor_visible = False
|
|
994
1018
|
|
|
995
1019
|
def claude_history_prompt_choice(
|
|
996
1020
|
self,
|
|
@@ -1052,6 +1076,7 @@ class TerminalSurface:
|
|
|
1052
1076
|
)
|
|
1053
1077
|
self.stream.write(b"".join(rendered))
|
|
1054
1078
|
self.stream.flush()
|
|
1079
|
+
self._painted_cursor_visible = False
|
|
1055
1080
|
|
|
1056
1081
|
def path_open_prompt_choice(
|
|
1057
1082
|
self,
|
|
@@ -1108,6 +1133,7 @@ class TerminalSurface:
|
|
|
1108
1133
|
# scroll the alternate screen.
|
|
1109
1134
|
self.stream.write(b"\033[0m\033[?7h\033[?25l")
|
|
1110
1135
|
self.stream.flush()
|
|
1136
|
+
self._painted_cursor_visible = False
|
|
1111
1137
|
|
|
1112
1138
|
@staticmethod
|
|
1113
1139
|
def _cursor_is_covered(
|
|
@@ -1171,38 +1197,37 @@ class TerminalSurface:
|
|
|
1171
1197
|
)
|
|
1172
1198
|
)
|
|
1173
1199
|
|
|
1174
|
-
@classmethod
|
|
1175
1200
|
def _append_cursor(
|
|
1176
|
-
|
|
1201
|
+
self,
|
|
1177
1202
|
rendered: list[bytes],
|
|
1178
1203
|
screen: AppliedScreen,
|
|
1179
1204
|
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1180
1205
|
*,
|
|
1181
1206
|
projection_top: int = 0,
|
|
1182
1207
|
visible_height: int | None = None,
|
|
1208
|
+
force_visibility: bool = False,
|
|
1183
1209
|
) -> None:
|
|
1184
1210
|
cursor_in_projection = (
|
|
1185
1211
|
visible_height is None
|
|
1186
1212
|
or projection_top <= screen.cursor_y < projection_top + visible_height
|
|
1187
1213
|
)
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
f"\033[{screen.cursor_y - projection_top + 1};"
|
|
1193
|
-
f"{screen.cursor_x + 1}H"
|
|
1194
|
-
).encode()
|
|
1195
|
-
if cursor_in_projection
|
|
1196
|
-
else b"\033[1;1H",
|
|
1197
|
-
(
|
|
1198
|
-
b"\033[?25h"
|
|
1199
|
-
if screen.cursor_visible
|
|
1200
|
-
and cursor_in_projection
|
|
1201
|
-
and not cls._cursor_is_covered(screen, overlays)
|
|
1202
|
-
else b"\033[?25l"
|
|
1203
|
-
),
|
|
1204
|
-
)
|
|
1214
|
+
visible = bool(
|
|
1215
|
+
screen.cursor_visible
|
|
1216
|
+
and cursor_in_projection
|
|
1217
|
+
and not self._cursor_is_covered(screen, overlays)
|
|
1205
1218
|
)
|
|
1219
|
+
rendered.extend((
|
|
1220
|
+
b"\033[0m\033[?7h",
|
|
1221
|
+
(
|
|
1222
|
+
f"\033[{screen.cursor_y - projection_top + 1};"
|
|
1223
|
+
f"{screen.cursor_x + 1}H"
|
|
1224
|
+
).encode()
|
|
1225
|
+
if cursor_in_projection
|
|
1226
|
+
else b"\033[1;1H",
|
|
1227
|
+
))
|
|
1228
|
+
if force_visibility or self._painted_cursor_visible is not visible:
|
|
1229
|
+
rendered.append(b"\033[?25h" if visible else b"\033[?25l")
|
|
1230
|
+
self._painted_cursor_visible = visible
|
|
1206
1231
|
|
|
1207
1232
|
def paint(
|
|
1208
1233
|
self,
|
|
@@ -1250,7 +1275,7 @@ class TerminalSurface:
|
|
|
1250
1275
|
visible_height=visible_height,
|
|
1251
1276
|
)
|
|
1252
1277
|
if self._local_status_text is not None:
|
|
1253
|
-
rendered.append(self._render_local_status())
|
|
1278
|
+
rendered.append(self._render_local_status(hide_cursor=False))
|
|
1254
1279
|
# Status and selection painting move and may temporarily hide the
|
|
1255
1280
|
# terminal cursor. Restoring the authoritative cursor must therefore
|
|
1256
1281
|
# be the final operation in every complete paint.
|
|
@@ -1289,7 +1314,61 @@ class TerminalSurface:
|
|
|
1289
1314
|
visible_height=visible_height,
|
|
1290
1315
|
)
|
|
1291
1316
|
if self._local_status_text is not None:
|
|
1292
|
-
rendered.append(self._render_local_status())
|
|
1317
|
+
rendered.append(self._render_local_status(hide_cursor=False))
|
|
1318
|
+
self._append_cursor(
|
|
1319
|
+
rendered,
|
|
1320
|
+
screen,
|
|
1321
|
+
overlays,
|
|
1322
|
+
projection_top=projection_top,
|
|
1323
|
+
visible_height=visible_height,
|
|
1324
|
+
)
|
|
1325
|
+
self.stream.write(b"".join(rendered))
|
|
1326
|
+
self.stream.flush()
|
|
1327
|
+
|
|
1328
|
+
def paint_changed_rows(
|
|
1329
|
+
self,
|
|
1330
|
+
screen: AppliedScreen,
|
|
1331
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1332
|
+
selection: tuple[SelectionSegment, ...],
|
|
1333
|
+
changed_rows: set[int] | frozenset[int],
|
|
1334
|
+
) -> None:
|
|
1335
|
+
"""Restore and repaint only rows touched by a local highlight.
|
|
1336
|
+
|
|
1337
|
+
Hover, drag selection, and click flash are local overlays. Clearing the
|
|
1338
|
+
whole alternate screen for them is both unnecessary and visibly flashes
|
|
1339
|
+
in Windows Terminal. Restore each affected row from the authoritative
|
|
1340
|
+
live frame, then layer frozen history and the new highlight back on it.
|
|
1341
|
+
"""
|
|
1342
|
+
self.start()
|
|
1343
|
+
self._last_screen = screen
|
|
1344
|
+
self._last_overlays = overlays
|
|
1345
|
+
projection_top, visible_height = self._projection(screen.height)
|
|
1346
|
+
rows = frozenset(
|
|
1347
|
+
row for row in changed_rows
|
|
1348
|
+
if projection_top <= row < projection_top + visible_height
|
|
1349
|
+
)
|
|
1350
|
+
rendered: list[bytes] = [b"\033[?7l"]
|
|
1351
|
+
for row in sorted(rows):
|
|
1352
|
+
rendered.extend((
|
|
1353
|
+
f"\033[{row - projection_top + 1};1H".encode(),
|
|
1354
|
+
b"\033[2K",
|
|
1355
|
+
screen.rows[row],
|
|
1356
|
+
))
|
|
1357
|
+
self._append_overlay_rows(
|
|
1358
|
+
rendered,
|
|
1359
|
+
overlays,
|
|
1360
|
+
projection_top=projection_top,
|
|
1361
|
+
visible_height=visible_height,
|
|
1362
|
+
changed_rows=rows,
|
|
1363
|
+
)
|
|
1364
|
+
self._append_selection_segments(
|
|
1365
|
+
rendered,
|
|
1366
|
+
selection,
|
|
1367
|
+
projection_top=projection_top,
|
|
1368
|
+
visible_height=visible_height,
|
|
1369
|
+
)
|
|
1370
|
+
if self._local_status_text is not None and screen.height - 1 in rows:
|
|
1371
|
+
rendered.append(self._render_local_status(hide_cursor=False))
|
|
1293
1372
|
self._append_cursor(
|
|
1294
1373
|
rendered,
|
|
1295
1374
|
screen,
|
|
@@ -1312,6 +1391,7 @@ class TerminalSurface:
|
|
|
1312
1391
|
self._last_overlays,
|
|
1313
1392
|
projection_top=projection_top,
|
|
1314
1393
|
visible_height=visible_height,
|
|
1394
|
+
force_visibility=True,
|
|
1315
1395
|
)
|
|
1316
1396
|
self.stream.write(b"".join(rendered))
|
|
1317
1397
|
self.stream.flush()
|
|
@@ -1333,6 +1413,7 @@ class TerminalSurface:
|
|
|
1333
1413
|
self.mouse_active = False
|
|
1334
1414
|
self.mouse_suspended = False
|
|
1335
1415
|
self.cursor_hidden = False
|
|
1416
|
+
self._painted_cursor_visible = None
|
|
1336
1417
|
self.clear_local_status()
|
|
1337
1418
|
self._last_screen = None
|
|
1338
1419
|
self._reconnect_status_screen = None
|
|
@@ -2627,11 +2708,14 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2627
2708
|
focused_pane_id=focused_pane_id,
|
|
2628
2709
|
)
|
|
2629
2710
|
)
|
|
2711
|
+
before = selection.segments()
|
|
2630
2712
|
if selection.hover(hover_event, source) and latest_screen is not None:
|
|
2631
|
-
|
|
2632
|
-
|
|
2713
|
+
after = selection.segments()
|
|
2714
|
+
surface.paint_changed_rows(
|
|
2715
|
+
latest_screen,
|
|
2633
2716
|
history.overlays(),
|
|
2634
|
-
|
|
2717
|
+
after,
|
|
2718
|
+
selection_changed_rows(before, after),
|
|
2635
2719
|
)
|
|
2636
2720
|
return
|
|
2637
2721
|
if path_open_prompt is not None:
|
|
@@ -2751,6 +2835,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2751
2835
|
# one persistent choice is still being confirmed.
|
|
2752
2836
|
return
|
|
2753
2837
|
if isinstance(part, SgrMouseEvent):
|
|
2838
|
+
selection_before = selection.segments()
|
|
2754
2839
|
displayed_height = (
|
|
2755
2840
|
latest_screen.height
|
|
2756
2841
|
if latest_screen is not None
|
|
@@ -2807,10 +2892,12 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2807
2892
|
if selection.capturing or not selection.active:
|
|
2808
2893
|
selection_clear_at = None
|
|
2809
2894
|
if selection_action.repaint and latest_screen is not None:
|
|
2810
|
-
|
|
2811
|
-
|
|
2895
|
+
selection_after = selection.segments()
|
|
2896
|
+
surface.paint_changed_rows(
|
|
2897
|
+
latest_screen,
|
|
2812
2898
|
history.overlays(),
|
|
2813
|
-
|
|
2899
|
+
selection_after,
|
|
2900
|
+
selection_changed_rows(selection_before, selection_after),
|
|
2814
2901
|
)
|
|
2815
2902
|
for replay_event in selection_action.replay_events:
|
|
2816
2903
|
if not claim_batched_wheel(replay_event, handled_wheels):
|
|
@@ -2835,6 +2922,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2835
2922
|
selection_clear_at = time.monotonic() + _SELECTION_HIGHLIGHT_SECONDS
|
|
2836
2923
|
if selection_action.open_target is not None:
|
|
2837
2924
|
target = selection_action.open_target
|
|
2925
|
+
flash_before = selection.segments()
|
|
2838
2926
|
if (
|
|
2839
2927
|
selection.flash(
|
|
2840
2928
|
target,
|
|
@@ -2843,10 +2931,12 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2843
2931
|
)
|
|
2844
2932
|
and latest_screen is not None
|
|
2845
2933
|
):
|
|
2846
|
-
|
|
2847
|
-
|
|
2934
|
+
flash_after = selection.segments()
|
|
2935
|
+
surface.paint_changed_rows(
|
|
2936
|
+
latest_screen,
|
|
2848
2937
|
history.overlays(),
|
|
2849
|
-
|
|
2938
|
+
flash_after,
|
|
2939
|
+
selection_changed_rows(flash_before, flash_after),
|
|
2850
2940
|
)
|
|
2851
2941
|
if target.kind == "url":
|
|
2852
2942
|
show_open_result(local_open.open_url(target.value))
|
|
@@ -3301,6 +3391,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3301
3391
|
restore_local_status = surface.expire_local_status(now) or (
|
|
3302
3392
|
history_info_until is not None and now >= history_info_until
|
|
3303
3393
|
)
|
|
3394
|
+
timed_selection_before = selection.segments()
|
|
3304
3395
|
clear_selection = (
|
|
3305
3396
|
selection_clear_at is not None and now >= selection_clear_at
|
|
3306
3397
|
)
|
|
@@ -3310,13 +3401,24 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3310
3401
|
selection.cancel()
|
|
3311
3402
|
if restore_local_status:
|
|
3312
3403
|
surface.clear_local_status()
|
|
3313
|
-
|
|
3404
|
+
timed_selection_after = selection.segments()
|
|
3405
|
+
if latest_screen is not None and restore_local_status:
|
|
3314
3406
|
surface.paint(
|
|
3315
3407
|
full_repaint(latest_screen),
|
|
3316
3408
|
history.overlays(),
|
|
3317
|
-
|
|
3409
|
+
timed_selection_after,
|
|
3318
3410
|
)
|
|
3319
3411
|
redraw_local_prompt()
|
|
3412
|
+
elif latest_screen is not None:
|
|
3413
|
+
surface.paint_changed_rows(
|
|
3414
|
+
latest_screen,
|
|
3415
|
+
history.overlays(),
|
|
3416
|
+
timed_selection_after,
|
|
3417
|
+
selection_changed_rows(
|
|
3418
|
+
timed_selection_before,
|
|
3419
|
+
timed_selection_after,
|
|
3420
|
+
),
|
|
3421
|
+
)
|
|
3320
3422
|
if restore_local_status:
|
|
3321
3423
|
history_info_until = None
|
|
3322
3424
|
if clear_selection:
|
|
@@ -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)
|
|
@@ -406,6 +406,7 @@ class _PaneGeometry:
|
|
|
406
406
|
transcript_provider: str | None = None
|
|
407
407
|
claude_history_policy: str = "ask"
|
|
408
408
|
history_generation: int = 0
|
|
409
|
+
canonical_history: bool = False
|
|
409
410
|
|
|
410
411
|
|
|
411
412
|
@dataclass(frozen=True)
|
|
@@ -432,11 +433,14 @@ _HISTORY_GENERATION_RE = re.compile(
|
|
|
432
433
|
|
|
433
434
|
def _history_generation(marker: str) -> int:
|
|
434
435
|
"""Map one validated provider UUID to a bounded opaque wire epoch."""
|
|
436
|
+
generation_marker = marker
|
|
437
|
+
if marker.startswith(tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX):
|
|
438
|
+
marker = marker[len(tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX):]
|
|
435
439
|
if not _HISTORY_GENERATION_RE.fullmatch(marker):
|
|
436
440
|
return 0
|
|
437
441
|
return int.from_bytes(
|
|
438
442
|
hashlib.blake2b(
|
|
439
|
-
|
|
443
|
+
generation_marker.lower().encode("ascii"),
|
|
440
444
|
digest_size=8,
|
|
441
445
|
person=b"railmux-history",
|
|
442
446
|
).digest(),
|
|
@@ -937,6 +941,15 @@ def _list_agent_panes(
|
|
|
937
941
|
if opened is not None:
|
|
938
942
|
transcript_locator = opened[0]
|
|
939
943
|
os.close(opened[1])
|
|
944
|
+
history_marker = fields[15]
|
|
945
|
+
canonical_history = bool(
|
|
946
|
+
transcript_backed
|
|
947
|
+
and transcript_locator is not None
|
|
948
|
+
and history_marker == (
|
|
949
|
+
f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}"
|
|
950
|
+
f"{transcript_locator.session_id}"
|
|
951
|
+
)
|
|
952
|
+
)
|
|
940
953
|
rows.append(
|
|
941
954
|
(
|
|
942
955
|
fields[2] == "1",
|
|
@@ -960,7 +973,8 @@ def _list_agent_panes(
|
|
|
960
973
|
else None
|
|
961
974
|
),
|
|
962
975
|
claude_history_policy=claude_history_policy,
|
|
963
|
-
history_generation=_history_generation(
|
|
976
|
+
history_generation=_history_generation(history_marker),
|
|
977
|
+
canonical_history=canonical_history,
|
|
964
978
|
),
|
|
965
979
|
)
|
|
966
980
|
)
|
|
@@ -1347,7 +1361,7 @@ def _capture_pane_history(
|
|
|
1347
1361
|
pane.transcript_backed
|
|
1348
1362
|
and pane.transcript_source
|
|
1349
1363
|
and (
|
|
1350
|
-
transcript_provider == "codex"
|
|
1364
|
+
(transcript_provider == "codex" and pane.canonical_history)
|
|
1351
1365
|
or (
|
|
1352
1366
|
transcript_provider == "claude"
|
|
1353
1367
|
and pane.claude_history_policy == "local"
|
|
@@ -75,6 +75,9 @@ class SessionMeta:
|
|
|
75
75
|
# conversation without changing the provider-owned UUID used for resume.
|
|
76
76
|
# Always None for Claude sessions and older Codex rollouts.
|
|
77
77
|
forked_from_id: str | None = None
|
|
78
|
+
# A Railmux lease observed in the same shared provider history root. This
|
|
79
|
+
# is a read-time UI overlay, never provider transcript metadata.
|
|
80
|
+
remote_owner: str | None = None
|
|
78
81
|
|
|
79
82
|
@property
|
|
80
83
|
def display_title(self) -> str:
|