railmux 0.4.0.dev6__tar.gz → 0.4.0.dev8__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.dev6/src/railmux.egg-info → railmux-0.4.0.dev8}/PKG-INFO +4 -2
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/README.md +3 -1
- railmux-0.4.0.dev8/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/fast_display_client.py +2 -2
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/fast_display_history.py +12 -3
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/fast_display_protocol.py +20 -7
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/fast_display_server.py +26 -3
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/tmux_ctl.py +49 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/app.py +89 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/windows_bootstrap.py +33 -8
- railmux-0.4.0.dev8/src/railmux/windows_install_log.py +388 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/windows_msys2.py +376 -100
- railmux-0.4.0.dev8/src/railmux/windows_pacman.py +402 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8/src/railmux.egg-info}/PKG-INFO +4 -2
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux.egg-info/SOURCES.txt +4 -0
- railmux-0.4.0.dev8/tests/test_codex_lineage.py +157 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_fast_full_window.py +125 -12
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tmux_ctl.py +37 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tmux_integration.py +41 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_windows_bootstrap.py +55 -0
- railmux-0.4.0.dev8/tests/test_windows_install_log.py +216 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_windows_msys2.py +110 -11
- railmux-0.4.0.dev8/tests/test_windows_pacman.py +288 -0
- railmux-0.4.0.dev6/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev6/tests/test_codex_lineage.py +0 -64
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/LICENSE +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/pyproject.toml +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/setup.cfg +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev6 → railmux-0.4.0.dev8}/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.dev8
|
|
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
|
|
@@ -374,7 +374,9 @@ messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
|
374
374
|
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
375
375
|
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
376
376
|
conversation: the retained prefix and replacement suffix remain visible, while
|
|
377
|
-
the abandoned suffix is hidden.
|
|
377
|
+
the abandoned suffix is hidden. When Railmux observes a running Codex session
|
|
378
|
+
advance to its rewind child, it also removes the abandoned terminal screen and
|
|
379
|
+
tmux scrollback before Codex redraws the live pane.
|
|
378
380
|
|
|
379
381
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
380
382
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -331,7 +331,9 @@ messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
|
331
331
|
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
332
332
|
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
333
333
|
conversation: the retained prefix and replacement suffix remain visible, while
|
|
334
|
-
the abandoned suffix is hidden.
|
|
334
|
+
the abandoned suffix is hidden. When Railmux observes a running Codex session
|
|
335
|
+
advance to its rewind child, it also removes the abandoned terminal screen and
|
|
336
|
+
tmux scrollback before Codex redraws the live pane.
|
|
335
337
|
|
|
336
338
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
337
339
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev8"
|
|
@@ -1074,7 +1074,7 @@ class TerminalSurface:
|
|
|
1074
1074
|
self,
|
|
1075
1075
|
requested: TerminalMode,
|
|
1076
1076
|
) -> TerminalMode:
|
|
1077
|
-
"""Mirror only input-affecting modes explicitly carried by protocol
|
|
1077
|
+
"""Mirror only input-affecting modes explicitly carried by protocol v15."""
|
|
1078
1078
|
disabled = self.terminal_modes & ~requested
|
|
1079
1079
|
enabled = requested & ~self.terminal_modes
|
|
1080
1080
|
controls: list[bytes] = []
|
|
@@ -2013,7 +2013,7 @@ def _finish_remote_attach(
|
|
|
2013
2013
|
raise ProbeError("remote display helper failed before attaching")
|
|
2014
2014
|
|
|
2015
2015
|
_stop_unstarted_remote(process)
|
|
2016
|
-
# A current
|
|
2016
|
+
# A current v15 helper holds the mutex only while registering its exact tmux
|
|
2017
2017
|
# child. Give that ordinary race one fresh SSH process before presenting
|
|
2018
2018
|
# the explicit legacy-lock takeover choice.
|
|
2019
2019
|
time.sleep(_REMOTE_ATTACH_RETRY_DELAY)
|
|
@@ -207,7 +207,11 @@ class LocalHistoryView:
|
|
|
207
207
|
restore_live = False
|
|
208
208
|
for pane_id, viewport in tuple(self.viewports.items()):
|
|
209
209
|
route = routes.get(pane_id)
|
|
210
|
-
if
|
|
210
|
+
if (
|
|
211
|
+
route is None
|
|
212
|
+
or not self._same_geometry(viewport.snapshot, route)
|
|
213
|
+
or not self._history_source_matches(viewport.snapshot, route)
|
|
214
|
+
):
|
|
211
215
|
self.cancel_pane(pane_id)
|
|
212
216
|
restore_live = True
|
|
213
217
|
return HistoryAction(restore_live=restore_live)
|
|
@@ -217,10 +221,11 @@ class LocalHistoryView:
|
|
|
217
221
|
left: HistorySnapshot,
|
|
218
222
|
right: HistorySnapshot,
|
|
219
223
|
) -> bool:
|
|
220
|
-
"""Do not combine
|
|
224
|
+
"""Do not combine different providers or terminal generations."""
|
|
221
225
|
return (
|
|
222
226
|
left.transcript_backed == right.transcript_backed
|
|
223
227
|
and left.history_choice_required == right.history_choice_required
|
|
228
|
+
and left.generation == right.generation
|
|
224
229
|
)
|
|
225
230
|
|
|
226
231
|
@staticmethod
|
|
@@ -813,7 +818,11 @@ class LocalHistoryView:
|
|
|
813
818
|
),
|
|
814
819
|
None,
|
|
815
820
|
)
|
|
816
|
-
if
|
|
821
|
+
if (
|
|
822
|
+
route is None
|
|
823
|
+
or not self._same_geometry(route, snapshot)
|
|
824
|
+
or not self._history_source_matches(route, snapshot)
|
|
825
|
+
):
|
|
817
826
|
return HistoryAction()
|
|
818
827
|
viewport = self.viewports.get(pending.pane_id)
|
|
819
828
|
if viewport is None:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Private
|
|
1
|
+
"""Private v15 framing for the coalesced full-window SSH display."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,9 +8,9 @@ from dataclasses import dataclass
|
|
|
8
8
|
from enum import IntEnum, IntFlag
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
DISPLAY_MAGIC = b"
|
|
12
|
-
INPUT_MAGIC = b"
|
|
13
|
-
PROTOCOL_VERSION =
|
|
11
|
+
DISPLAY_MAGIC = b"RMUXD15\x00"
|
|
12
|
+
INPUT_MAGIC = b"RMUXK15\x00"
|
|
13
|
+
PROTOCOL_VERSION = 15
|
|
14
14
|
REMOTE_CONFIG_PROTOCOL = 1
|
|
15
15
|
LENGTH_BYTES = 4
|
|
16
16
|
REMOTE_HELLO_PREFIX = b"RAILMUX-REMOTE/1 "
|
|
@@ -28,10 +28,10 @@ MAX_HISTORY_PANES = 8
|
|
|
28
28
|
MAX_CLIPBOARD_BYTES = 64 * 1024
|
|
29
29
|
MAX_PATH_BYTES = 4096
|
|
30
30
|
_UPDATE_METADATA = struct.Struct(">BIHHHHBHI")
|
|
31
|
-
_HISTORY_METADATA = struct.Struct(">
|
|
31
|
+
_HISTORY_METADATA = struct.Struct(">IIHHHHBQI")
|
|
32
32
|
_HISTORY_REQUEST = struct.Struct(">IHHH")
|
|
33
33
|
_HISTORY_BATCH_METADATA = struct.Struct(">II")
|
|
34
|
-
_HISTORY_PANE_METADATA = struct.Struct(">
|
|
34
|
+
_HISTORY_PANE_METADATA = struct.Struct(">IHHHHBQ")
|
|
35
35
|
_PREFETCH_HISTORY_REQUEST = struct.Struct(">IH")
|
|
36
36
|
_PATH_REQUEST = struct.Struct(">IIH")
|
|
37
37
|
_PATH_RESULT = struct.Struct(">IBBH")
|
|
@@ -143,6 +143,7 @@ class HistorySnapshot:
|
|
|
143
143
|
more_available: bool = False
|
|
144
144
|
transcript_available: bool = False
|
|
145
145
|
history_choice_required: bool = False
|
|
146
|
+
generation: int = 0
|
|
146
147
|
|
|
147
148
|
|
|
148
149
|
@dataclass(frozen=True)
|
|
@@ -302,6 +303,7 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
|
|
|
302
303
|
snapshot.more_available,
|
|
303
304
|
snapshot.transcript_available,
|
|
304
305
|
snapshot.history_choice_required,
|
|
306
|
+
snapshot.generation,
|
|
305
307
|
)):
|
|
306
308
|
raise ValueError("a rejected history response must be empty")
|
|
307
309
|
pane_number = 0
|
|
@@ -317,6 +319,8 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
|
|
|
317
319
|
raise ValueError("invalid history position")
|
|
318
320
|
if len(snapshot.lines) < snapshot.height:
|
|
319
321
|
raise ValueError("history snapshot is shorter than its viewport")
|
|
322
|
+
if not 0 <= snapshot.generation <= 0xFFFFFFFFFFFFFFFF:
|
|
323
|
+
raise ValueError("invalid history generation")
|
|
320
324
|
raw_lines = _pack_history_lines(snapshot.lines)
|
|
321
325
|
compressed = zlib.compress(raw_lines, level=3)
|
|
322
326
|
metadata = _HISTORY_METADATA.pack(
|
|
@@ -327,6 +331,7 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
|
|
|
327
331
|
snapshot.width,
|
|
328
332
|
snapshot.height,
|
|
329
333
|
_history_capabilities(snapshot),
|
|
334
|
+
snapshot.generation,
|
|
330
335
|
len(raw_lines),
|
|
331
336
|
)
|
|
332
337
|
payload = bytes((int(OutputKind.HISTORY),)) + metadata + compressed
|
|
@@ -360,6 +365,8 @@ def encode_history_batch(batch: HistoryBatch) -> bytes:
|
|
|
360
365
|
raise ValueError("invalid history position")
|
|
361
366
|
if len(snapshot.lines) < snapshot.height:
|
|
362
367
|
raise ValueError("history snapshot is shorter than its viewport")
|
|
368
|
+
if not 0 <= snapshot.generation <= 0xFFFFFFFFFFFFFFFF:
|
|
369
|
+
raise ValueError("invalid history generation")
|
|
363
370
|
packed_lines = _pack_history_lines(snapshot.lines)
|
|
364
371
|
pane_metadata = _HISTORY_PANE_METADATA.pack(
|
|
365
372
|
pane_number,
|
|
@@ -368,6 +375,7 @@ def encode_history_batch(batch: HistoryBatch) -> bytes:
|
|
|
368
375
|
snapshot.width,
|
|
369
376
|
snapshot.height,
|
|
370
377
|
_history_capabilities(snapshot),
|
|
378
|
+
snapshot.generation,
|
|
371
379
|
)
|
|
372
380
|
total += len(pane_metadata) + len(packed_lines)
|
|
373
381
|
if total > MAX_SCREEN_BYTES:
|
|
@@ -611,6 +619,7 @@ class ServerMessageDecoder:
|
|
|
611
619
|
width,
|
|
612
620
|
height,
|
|
613
621
|
raw_capabilities,
|
|
622
|
+
generation,
|
|
614
623
|
raw_size,
|
|
615
624
|
) = (
|
|
616
625
|
_HISTORY_METADATA.unpack(body[:_HISTORY_METADATA.size])
|
|
@@ -628,6 +637,7 @@ class ServerMessageDecoder:
|
|
|
628
637
|
height,
|
|
629
638
|
lines,
|
|
630
639
|
raw_capabilities,
|
|
640
|
+
generation,
|
|
631
641
|
)):
|
|
632
642
|
raise ValueError("invalid rejected history response")
|
|
633
643
|
pane_id = None
|
|
@@ -662,6 +672,7 @@ class ServerMessageDecoder:
|
|
|
662
672
|
history_choice_required=bool(
|
|
663
673
|
raw_capabilities & _HISTORY_CHOICE_REQUIRED
|
|
664
674
|
),
|
|
675
|
+
generation=generation,
|
|
665
676
|
))
|
|
666
677
|
continue
|
|
667
678
|
if output_kind is OutputKind.HISTORY_BATCH:
|
|
@@ -691,6 +702,7 @@ class ServerMessageDecoder:
|
|
|
691
702
|
width,
|
|
692
703
|
height,
|
|
693
704
|
raw_capabilities,
|
|
705
|
+
generation,
|
|
694
706
|
) = _HISTORY_PANE_METADATA.unpack(
|
|
695
707
|
raw[offset:offset + _HISTORY_PANE_METADATA.size]
|
|
696
708
|
)
|
|
@@ -729,6 +741,7 @@ class ServerMessageDecoder:
|
|
|
729
741
|
history_choice_required=bool(
|
|
730
742
|
raw_capabilities & _HISTORY_CHOICE_REQUIRED
|
|
731
743
|
),
|
|
744
|
+
generation=generation,
|
|
732
745
|
))
|
|
733
746
|
if offset != len(raw):
|
|
734
747
|
raise ValueError("trailing history batch data")
|
|
@@ -843,7 +856,7 @@ class ServerMessageDecoder:
|
|
|
843
856
|
|
|
844
857
|
|
|
845
858
|
class ScreenUpdateDecoder:
|
|
846
|
-
"""Compatibility view which ignores
|
|
859
|
+
"""Compatibility view which ignores v15 control-response messages."""
|
|
847
860
|
|
|
848
861
|
def __init__(self) -> None:
|
|
849
862
|
self._decoder = ServerMessageDecoder()
|
|
@@ -45,6 +45,7 @@ from railmux import (
|
|
|
45
45
|
__version__,
|
|
46
46
|
local_open,
|
|
47
47
|
restart_state,
|
|
48
|
+
tmux_ctl,
|
|
48
49
|
tmux_health,
|
|
49
50
|
tmux_server,
|
|
50
51
|
)
|
|
@@ -403,6 +404,7 @@ class _PaneGeometry:
|
|
|
403
404
|
transcript_source: str | None = None
|
|
404
405
|
transcript_backed: bool = False
|
|
405
406
|
claude_history_policy: str = "ask"
|
|
407
|
+
history_generation: int = 0
|
|
406
408
|
|
|
407
409
|
|
|
408
410
|
@dataclass(frozen=True)
|
|
@@ -421,6 +423,24 @@ _INFERRED_TRANSCRIPT_LIMIT = 8
|
|
|
421
423
|
_TRANSCRIPT_MAX_BYTES = 32 * 1024 * 1024
|
|
422
424
|
_SESSION_BINDING_OPTION = "@railmux_binding_v1"
|
|
423
425
|
_SWAP_OPTIONS = ("@railmux_swap_primary", "@railmux_swap_secondary")
|
|
426
|
+
_HISTORY_GENERATION_RE = re.compile(
|
|
427
|
+
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
428
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _history_generation(marker: str) -> int:
|
|
433
|
+
"""Map one validated provider UUID to a bounded opaque wire epoch."""
|
|
434
|
+
if not _HISTORY_GENERATION_RE.fullmatch(marker):
|
|
435
|
+
return 0
|
|
436
|
+
return int.from_bytes(
|
|
437
|
+
hashlib.blake2b(
|
|
438
|
+
marker.lower().encode("ascii"),
|
|
439
|
+
digest_size=8,
|
|
440
|
+
person=b"railmux-history",
|
|
441
|
+
).digest(),
|
|
442
|
+
"big",
|
|
443
|
+
)
|
|
424
444
|
|
|
425
445
|
|
|
426
446
|
_ANSI_FG = {
|
|
@@ -834,6 +854,7 @@ def _list_agent_panes(
|
|
|
834
854
|
"#{history_size} #{alternate_on} #{mouse_any_flag} "
|
|
835
855
|
f"#{{{tmux_server.HISTORY_SOURCE_OPTION}}} "
|
|
836
856
|
f"#{{{tmux_server.TRANSCRIPT_SOURCE_OPTION}}} "
|
|
857
|
+
f"#{{{tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION}}} "
|
|
837
858
|
f"#{{{TOOL_PANE_OPTION}}}",
|
|
838
859
|
),
|
|
839
860
|
stderr=subprocess.DEVNULL,
|
|
@@ -849,7 +870,7 @@ def _list_agent_panes(
|
|
|
849
870
|
for raw_row in output.splitlines():
|
|
850
871
|
fields = raw_row.split(" ")
|
|
851
872
|
if (
|
|
852
|
-
len(fields) !=
|
|
873
|
+
len(fields) != 17
|
|
853
874
|
or fields[0] != session_id
|
|
854
875
|
or fields[2] not in ("0", "1")
|
|
855
876
|
or fields[3] not in ("0", "1")
|
|
@@ -880,7 +901,7 @@ def _list_agent_panes(
|
|
|
880
901
|
):
|
|
881
902
|
return ()
|
|
882
903
|
seen.add(pane_id)
|
|
883
|
-
if is_tool_pane_marker(fields[
|
|
904
|
+
if is_tool_pane_marker(fields[16], outer_session_id=session_id):
|
|
884
905
|
continue
|
|
885
906
|
history_server = None
|
|
886
907
|
history_pane_id = None
|
|
@@ -931,6 +952,7 @@ def _list_agent_panes(
|
|
|
931
952
|
transcript_source=transcript_marker,
|
|
932
953
|
transcript_backed=transcript_backed,
|
|
933
954
|
claude_history_policy=claude_history_policy,
|
|
955
|
+
history_generation=_history_generation(fields[15]),
|
|
934
956
|
),
|
|
935
957
|
)
|
|
936
958
|
)
|
|
@@ -1376,6 +1398,7 @@ def _capture_pane_history(
|
|
|
1376
1398
|
else pane.history_size + pane.height > len(lines)
|
|
1377
1399
|
)
|
|
1378
1400
|
),
|
|
1401
|
+
generation=pane.history_generation,
|
|
1379
1402
|
)
|
|
1380
1403
|
|
|
1381
1404
|
|
|
@@ -1916,7 +1939,7 @@ def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
|
1916
1939
|
|
|
1917
1940
|
|
|
1918
1941
|
def terminal_modes_for_screen(screen: object) -> TerminalMode:
|
|
1919
|
-
"""Project pyte's private-mode set onto the bounded
|
|
1942
|
+
"""Project pyte's private-mode set onto the bounded v15 wire allowlist."""
|
|
1920
1943
|
terminal_modes = TerminalMode.NONE
|
|
1921
1944
|
if 2004 << 5 in screen.mode:
|
|
1922
1945
|
terminal_modes |= TerminalMode.BRACKETED_PASTE
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
|
+
import signal
|
|
11
12
|
import shlex
|
|
12
13
|
import shutil
|
|
13
14
|
import subprocess
|
|
@@ -302,6 +303,12 @@ _ROLLOUT_UUID_RE = re.compile(
|
|
|
302
303
|
)
|
|
303
304
|
|
|
304
305
|
|
|
306
|
+
def rollout_uuid_from_path(path: Path) -> str | None:
|
|
307
|
+
"""Return the filename UUID for a canonical Codex rollout path."""
|
|
308
|
+
match = _ROLLOUT_UUID_RE.search(path.name)
|
|
309
|
+
return match.group(1) if match is not None else None
|
|
310
|
+
|
|
311
|
+
|
|
305
312
|
def pane_pid_for_session(session_name: str) -> int | None:
|
|
306
313
|
"""Return the first pane's PID for *session_name*, or None.
|
|
307
314
|
|
|
@@ -1818,6 +1825,7 @@ _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
|
1818
1825
|
RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
|
|
1819
1826
|
RAILMUX_TARGET_OPTION = "@railmux_target_pane"
|
|
1820
1827
|
RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
|
|
1828
|
+
RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
|
|
1821
1829
|
# tmux names function keys only through F12. Private controller routes use
|
|
1822
1830
|
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1823
1831
|
# user input forwarded to an agent pane.
|
|
@@ -3347,6 +3355,47 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
|
|
|
3347
3355
|
)
|
|
3348
3356
|
|
|
3349
3357
|
|
|
3358
|
+
def reset_pane_history(identity: PaneIdentity) -> bool:
|
|
3359
|
+
"""Reset visible bytes and scrollback for the exact observed live pane.
|
|
3360
|
+
|
|
3361
|
+
Revalidate the immutable session, pane process, and window before issuing
|
|
3362
|
+
one tmux command queue that resets the terminal model and clears history.
|
|
3363
|
+
Then send the pane's foreground process group an ordinary same-size
|
|
3364
|
+
``SIGWINCH`` so its TUI repaints and reasserts terminal modes without a
|
|
3365
|
+
geometry change or injected keyboard input.
|
|
3366
|
+
"""
|
|
3367
|
+
current = pane_identity(identity.pane_id)
|
|
3368
|
+
if (current is None or current.dead
|
|
3369
|
+
or current.pane_pid != identity.pane_pid
|
|
3370
|
+
or current.session_id != identity.session_id
|
|
3371
|
+
or current.window_id != identity.window_id):
|
|
3372
|
+
return False
|
|
3373
|
+
try:
|
|
3374
|
+
subprocess.check_call(
|
|
3375
|
+
[
|
|
3376
|
+
"tmux", "send-keys", "-R", "-t", identity.pane_id,
|
|
3377
|
+
";", "clear-history", "-t", identity.pane_id,
|
|
3378
|
+
],
|
|
3379
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
3380
|
+
)
|
|
3381
|
+
except (OSError, subprocess.CalledProcessError):
|
|
3382
|
+
return False
|
|
3383
|
+
|
|
3384
|
+
current = pane_identity(identity.pane_id)
|
|
3385
|
+
if (current is None or current.dead
|
|
3386
|
+
or current.pane_pid != identity.pane_pid
|
|
3387
|
+
or current.session_id != identity.session_id
|
|
3388
|
+
or current.window_id != identity.window_id):
|
|
3389
|
+
return True
|
|
3390
|
+
try:
|
|
3391
|
+
os.killpg(os.getpgid(current.pane_pid), signal.SIGWINCH)
|
|
3392
|
+
except (OSError, ValueError):
|
|
3393
|
+
# The presentation cache is already clean. A provider that exited or
|
|
3394
|
+
# cannot be signalled will repaint on its next ordinary terminal write.
|
|
3395
|
+
pass
|
|
3396
|
+
return True
|
|
3397
|
+
|
|
3398
|
+
|
|
3350
3399
|
def kill_session_identity(identity: PaneIdentity) -> bool:
|
|
3351
3400
|
"""Kill an exact immutable tmux session, never a reused session name."""
|
|
3352
3401
|
current = pane_identity(identity.pane_id)
|
|
@@ -365,6 +365,12 @@ class _Running:
|
|
|
365
365
|
legacy_server: tmux_server.TmuxServerTarget | None = None
|
|
366
366
|
legacy_session_id: str | None = None
|
|
367
367
|
provider_session_id: str | None = None
|
|
368
|
+
# Last canonical Codex rollout observed for this live pane. Rewind creates
|
|
369
|
+
# a child rollout while leaving tmux's byte scrollback intact; tracking the
|
|
370
|
+
# provider generation lets the refresh path discard only that stale
|
|
371
|
+
# presentation cache when the exact live writer advances to its child.
|
|
372
|
+
codex_canonical_session_id: str | None = None
|
|
373
|
+
codex_history_generation_stamped: bool = False
|
|
368
374
|
|
|
369
375
|
@property
|
|
370
376
|
def is_placeholder(self) -> bool:
|
|
@@ -8530,6 +8536,9 @@ class App:
|
|
|
8530
8536
|
meta = self._session_cache.get(
|
|
8531
8537
|
r.project, r.logical_session_id or r.key)
|
|
8532
8538
|
if meta is not None:
|
|
8539
|
+
if session_type == "codex":
|
|
8540
|
+
self._sync_codex_rewind_scrollback(
|
|
8541
|
+
r, meta, server, codex_rollout_probes)
|
|
8533
8542
|
if meta.title:
|
|
8534
8543
|
r.label = f"{meta.project.display_name}/{meta.display_title}"
|
|
8535
8544
|
r.status = self._effective_status(
|
|
@@ -8548,6 +8557,86 @@ class App:
|
|
|
8548
8557
|
self._maybe_resort_running()
|
|
8549
8558
|
self._render_running_pane()
|
|
8550
8559
|
|
|
8560
|
+
def _sync_codex_rewind_scrollback(
|
|
8561
|
+
self,
|
|
8562
|
+
running: _Running,
|
|
8563
|
+
meta: SessionMeta,
|
|
8564
|
+
server: tmux_ctl.ServerSnapshot | None,
|
|
8565
|
+
codex_rollout_probes: dict[str, set[str] | None] | None,
|
|
8566
|
+
) -> None:
|
|
8567
|
+
"""Drop stale tmux bytes after an exact live Codex rewind transition.
|
|
8568
|
+
|
|
8569
|
+
The provider rollout is the history authority, but Codex keeps using
|
|
8570
|
+
the same terminal pane after a rewind. Its abandoned suffix can
|
|
8571
|
+
therefore survive in tmux scrollback even though the canonical JSONL
|
|
8572
|
+
is already correct. Baseline the first observed rollout, then clear
|
|
8573
|
+
only when the next canonical rollout names that baseline as its parent.
|
|
8574
|
+
On procfs systems the new rollout must also be open in this exact pane's
|
|
8575
|
+
process tree; an unavailable probe degrades to the provider lineage
|
|
8576
|
+
link, while a negative probe waits for a later refresh.
|
|
8577
|
+
"""
|
|
8578
|
+
current_id = meta.session_id
|
|
8579
|
+
previous_id = running.codex_canonical_session_id
|
|
8580
|
+
if previous_id is None:
|
|
8581
|
+
running.codex_canonical_session_id = current_id
|
|
8582
|
+
self._stamp_codex_history_generation(running, current_id)
|
|
8583
|
+
return
|
|
8584
|
+
if current_id == previous_id:
|
|
8585
|
+
if not running.codex_history_generation_stamped:
|
|
8586
|
+
self._stamp_codex_history_generation(running, current_id)
|
|
8587
|
+
return
|
|
8588
|
+
if (running.is_legacy or meta.forked_from_id != previous_id
|
|
8589
|
+
or current_id not in self._codex_lineage_ids(previous_id)):
|
|
8590
|
+
return
|
|
8591
|
+
|
|
8592
|
+
rollout_id = tmux_ctl.rollout_uuid_from_path(meta.jsonl_path)
|
|
8593
|
+
if rollout_id != current_id:
|
|
8594
|
+
return
|
|
8595
|
+
probes = codex_rollout_probes if codex_rollout_probes is not None else {}
|
|
8596
|
+
if running.tmux_name in probes:
|
|
8597
|
+
open_ids = probes[running.tmux_name]
|
|
8598
|
+
else:
|
|
8599
|
+
open_ids = self._codex_rollout_ids(running, server)
|
|
8600
|
+
probes[running.tmux_name] = open_ids
|
|
8601
|
+
if open_ids is not None and current_id not in open_ids:
|
|
8602
|
+
return
|
|
8603
|
+
|
|
8604
|
+
identity = self._codex_real_pane_identity(running)
|
|
8605
|
+
if identity is None or not tmux_ctl.reset_pane_history(identity):
|
|
8606
|
+
return
|
|
8607
|
+
running.codex_canonical_session_id = current_id
|
|
8608
|
+
running.codex_history_generation_stamped = tmux_ctl.set_pane_user_option(
|
|
8609
|
+
identity.pane_id,
|
|
8610
|
+
tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
|
|
8611
|
+
current_id,
|
|
8612
|
+
)
|
|
8613
|
+
|
|
8614
|
+
def _codex_real_pane_identity(
|
|
8615
|
+
self, running: _Running,
|
|
8616
|
+
) -> tmux_ctl.PaneIdentity | None:
|
|
8617
|
+
"""Resolve a Codex pane across its detached home or swap location."""
|
|
8618
|
+
real_pane = self._display_transport().displayed_real_pane(
|
|
8619
|
+
running.tmux_name)
|
|
8620
|
+
if real_pane is not None:
|
|
8621
|
+
return tmux_ctl.pane_identity(real_pane)
|
|
8622
|
+
topology = tmux_ctl.session_topology(running.tmux_name)
|
|
8623
|
+
return topology.single_live_pane if topology is not None else None
|
|
8624
|
+
|
|
8625
|
+
def _stamp_codex_history_generation(
|
|
8626
|
+
self, running: _Running, session_id: str,
|
|
8627
|
+
) -> None:
|
|
8628
|
+
"""Publish a content-free epoch for full-window SSH history caches."""
|
|
8629
|
+
if running.is_legacy:
|
|
8630
|
+
return
|
|
8631
|
+
identity = self._codex_real_pane_identity(running)
|
|
8632
|
+
if identity is None:
|
|
8633
|
+
return
|
|
8634
|
+
running.codex_history_generation_stamped = tmux_ctl.set_pane_user_option(
|
|
8635
|
+
identity.pane_id,
|
|
8636
|
+
tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
|
|
8637
|
+
session_id,
|
|
8638
|
+
)
|
|
8639
|
+
|
|
8551
8640
|
def _maybe_resort_running(self) -> None:
|
|
8552
8641
|
"""Re-order the Running registry by recency, at most once per minute.
|
|
8553
8642
|
|
|
@@ -26,7 +26,7 @@ def _print_help() -> None:
|
|
|
26
26
|
" railmux ssh HOST [OPTIONS]\n"
|
|
27
27
|
" railmux config [--remote HOST] [OPTIONS]\n"
|
|
28
28
|
" railmux doctor [--remote HOST] [OPTIONS]\n"
|
|
29
|
-
" railmux runtime {status,install} [--yes]\n\n"
|
|
29
|
+
" railmux runtime {status,install} [--yes] [--verbose]\n\n"
|
|
30
30
|
"Windows preview: Railmux runs in a private managed MSYS2/tmux "
|
|
31
31
|
"runtime while Codex and Claude Code remain Windows-native and use "
|
|
32
32
|
"the existing Windows user session directories."
|
|
@@ -55,8 +55,11 @@ def _runtime_status(
|
|
|
55
55
|
def _confirm_install(*, input_fn: Callable[[str], str] = input) -> bool:
|
|
56
56
|
try:
|
|
57
57
|
answer = input_fn(
|
|
58
|
-
"Install the private MSYS2/tmux runtime now? "
|
|
59
|
-
"
|
|
58
|
+
"Install the private MSYS2/tmux runtime now? On Windows, Railmux "
|
|
59
|
+
"depends on a complete private MSYS2 compatibility wrapper, "
|
|
60
|
+
"including tmux and Python. This downloads a 50 MB base plus "
|
|
61
|
+
"required updates and packages, "
|
|
62
|
+
"and uses about 700 MB or more of private disk space [y/N] "
|
|
60
63
|
)
|
|
61
64
|
except (EOFError, KeyboardInterrupt):
|
|
62
65
|
print()
|
|
@@ -68,13 +71,18 @@ def _install(
|
|
|
68
71
|
*,
|
|
69
72
|
environ: Mapping[str, str],
|
|
70
73
|
assume_yes: bool,
|
|
74
|
+
verbose: bool = False,
|
|
71
75
|
input_fn: Callable[[str], str] = input,
|
|
72
76
|
) -> Msys2Runtime | None:
|
|
73
77
|
if not assume_yes and not _confirm_install(input_fn=input_fn):
|
|
74
78
|
print("MSYS2 runtime installation cancelled.", file=sys.stderr)
|
|
75
79
|
return None
|
|
76
80
|
try:
|
|
77
|
-
runtime = install_managed_runtime(
|
|
81
|
+
runtime = install_managed_runtime(
|
|
82
|
+
version=__version__,
|
|
83
|
+
environ=environ,
|
|
84
|
+
verbose=verbose,
|
|
85
|
+
)
|
|
78
86
|
except RuntimeInstallError as exc:
|
|
79
87
|
print(f"error: {exc}", file=sys.stderr)
|
|
80
88
|
return None
|
|
@@ -135,12 +143,24 @@ def main(
|
|
|
135
143
|
if arguments[:2] == ["runtime", "status"] and len(arguments) == 2:
|
|
136
144
|
return _runtime_status(runtime, environ=environ)
|
|
137
145
|
if arguments and arguments[0] == "runtime":
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
install_arguments = arguments[1:]
|
|
147
|
+
install_flags = install_arguments[1:] if install_arguments[:1] == ["install"] else []
|
|
148
|
+
if (
|
|
149
|
+
not install_arguments
|
|
150
|
+
or install_arguments[0] != "install"
|
|
151
|
+
or len(install_flags) != len(set(install_flags))
|
|
152
|
+
or any(flag not in {"--yes", "--verbose"} for flag in install_flags)
|
|
153
|
+
):
|
|
154
|
+
print(
|
|
155
|
+
"error: usage: railmux runtime {status,install} "
|
|
156
|
+
"[--yes] [--verbose]",
|
|
157
|
+
file=sys.stderr,
|
|
158
|
+
)
|
|
140
159
|
return 2
|
|
141
160
|
runtime = runtime or _install(
|
|
142
161
|
environ=environ,
|
|
143
|
-
assume_yes=
|
|
162
|
+
assume_yes="--yes" in install_flags,
|
|
163
|
+
verbose="--verbose" in install_flags,
|
|
144
164
|
input_fn=input_fn,
|
|
145
165
|
)
|
|
146
166
|
return 0 if runtime is not None else 2
|
|
@@ -162,7 +182,12 @@ def main(
|
|
|
162
182
|
file=sys.stderr,
|
|
163
183
|
)
|
|
164
184
|
return 2
|
|
165
|
-
runtime = _install(
|
|
185
|
+
runtime = _install(
|
|
186
|
+
environ=environ,
|
|
187
|
+
assume_yes=False,
|
|
188
|
+
verbose=False,
|
|
189
|
+
input_fn=input_fn,
|
|
190
|
+
)
|
|
166
191
|
if runtime is None:
|
|
167
192
|
return 2
|
|
168
193
|
|