railmux 0.3.3.dev2__tar.gz → 0.3.3.dev4__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.3.3.dev2/src/railmux.egg-info → railmux-0.3.3.dev4}/PKG-INFO +21 -11
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/README.md +20 -10
- railmux-0.3.3.dev4/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_client.py +2 -2
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_history.py +12 -3
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_protocol.py +20 -7
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_server.py +26 -3
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/mouse_manager.py +8 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_ctl.py +131 -7
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/transcript.py +83 -28
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/app.py +230 -15
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/modals.py +1 -1
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4/src/railmux.egg-info}/PKG-INFO +21 -11
- railmux-0.3.3.dev4/tests/test_codex_lineage.py +157 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_fast_full_window.py +125 -12
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_focus_highlight.py +135 -8
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_mouse_manager.py +17 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_release_workflow.py +25 -8
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_soft_quit.py +42 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_ctl.py +65 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_integration.py +42 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_transcript.py +84 -0
- railmux-0.3.3.dev2/src/railmux/__init__.py +0 -1
- railmux-0.3.3.dev2/tests/test_codex_lineage.py +0 -64
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/LICENSE +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/pyproject.toml +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/setup.cfg +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_display_transport.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_statusbar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tools/release_notes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.3.
|
|
3
|
+
Version: 0.3.3.dev4
|
|
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
|
|
@@ -169,12 +169,14 @@ for focus and layout limits.
|
|
|
169
169
|
|--------|--------|
|
|
170
170
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
171
171
|
| Left-click (running) | Switch the Target pane to that session |
|
|
172
|
+
| Wheel up over a live agent | Open its canonical current-branch history |
|
|
172
173
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
173
174
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
174
175
|
|
|
175
176
|
Unavailable context-menu actions are hidden for the selected session state.
|
|
176
|
-
Single-click and `␣` act in the Target pane without moving keyboard focus
|
|
177
|
-
|
|
177
|
+
Single-click and `␣` act in the Target pane without moving keyboard focus, but
|
|
178
|
+
`␣` always means history while a click on a running row means live attach.
|
|
179
|
+
Double-click and `Enter` also move focus there. The terminal must report mouse
|
|
178
180
|
buttons to applications for these actions to reach Railmux. Right-click
|
|
179
181
|
reporting is sometimes a separate setting from ordinary mouse reporting; see
|
|
180
182
|
[FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
|
|
@@ -207,7 +209,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
207
209
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
208
210
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
209
211
|
| `o` | Open persistent Railmux options |
|
|
210
|
-
| `␣` | Preview stopped or
|
|
212
|
+
| `␣` | Preview canonical history for the selected stopped or running session |
|
|
211
213
|
| `F8` | Cycle agent layout: single → side-by-side → stacked |
|
|
212
214
|
| `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
|
|
213
215
|
| `?` | Full help popup with all keybindings |
|
|
@@ -299,7 +301,8 @@ to single leaves Pane 2's agent running in the background.
|
|
|
299
301
|
|
|
300
302
|
In a split, focus an agent pane to make it the Target pane. After focus returns
|
|
301
303
|
to the sidebar, single-click or `␣` acts in that pane without moving keyboard
|
|
302
|
-
focus;
|
|
304
|
+
focus; a running-row click attaches live while `␣` previews history.
|
|
305
|
+
Double-click or `Enter` opens there and transfers focus. The status bar
|
|
303
306
|
at the bottom-left shows the current layout and Target pane:
|
|
304
307
|
|
|
305
308
|
| Symbol | Meaning |
|
|
@@ -363,15 +366,22 @@ sessions move ahead of the other results.
|
|
|
363
366
|
## History preview
|
|
364
367
|
|
|
365
368
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
366
|
-
the Target pane without starting or resuming the agent.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
the Target pane without starting or resuming the agent. For a running session,
|
|
370
|
+
press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
|
|
371
|
+
read-only: it cannot send a message or change the session. User and assistant
|
|
372
|
+
messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
373
|
+
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
374
|
+
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
375
|
+
conversation: the retained prefix and replacement suffix remain visible, while
|
|
376
|
+
the abandoned suffix is hidden. When Railmux observes a running Codex session
|
|
377
|
+
advance to its rewind child, it also removes the abandoned terminal screen and
|
|
378
|
+
tmux scrollback before Codex redraws the live pane.
|
|
370
379
|
|
|
371
380
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
372
381
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
373
|
-
matches, and `q` to exit
|
|
374
|
-
|
|
382
|
+
matches, and `q` to exit. If preview began from a live pane, `q` restores that
|
|
383
|
+
exact session; otherwise it restores the prior Target pane. Double-click to
|
|
384
|
+
skip preview and open the session directly.
|
|
375
385
|
|
|
376
386
|
## Status indicators
|
|
377
387
|
|
|
@@ -127,12 +127,14 @@ 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 up over a live agent | Open its canonical current-branch history |
|
|
130
131
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
131
132
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
132
133
|
|
|
133
134
|
Unavailable context-menu actions are hidden for the selected session state.
|
|
134
|
-
Single-click and `␣` act in the Target pane without moving keyboard focus
|
|
135
|
-
|
|
135
|
+
Single-click and `␣` act in the Target pane without moving keyboard focus, but
|
|
136
|
+
`␣` always means history while a click on a running row means live attach.
|
|
137
|
+
Double-click and `Enter` also move focus there. The terminal must report mouse
|
|
136
138
|
buttons to applications for these actions to reach Railmux. Right-click
|
|
137
139
|
reporting is sometimes a separate setting from ordinary mouse reporting; see
|
|
138
140
|
[FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
|
|
@@ -165,7 +167,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
165
167
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
166
168
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
167
169
|
| `o` | Open persistent Railmux options |
|
|
168
|
-
| `␣` | Preview stopped or
|
|
170
|
+
| `␣` | Preview canonical history for the selected stopped or running session |
|
|
169
171
|
| `F8` | Cycle agent layout: single → side-by-side → stacked |
|
|
170
172
|
| `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
|
|
171
173
|
| `?` | Full help popup with all keybindings |
|
|
@@ -257,7 +259,8 @@ to single leaves Pane 2's agent running in the background.
|
|
|
257
259
|
|
|
258
260
|
In a split, focus an agent pane to make it the Target pane. After focus returns
|
|
259
261
|
to the sidebar, single-click or `␣` acts in that pane without moving keyboard
|
|
260
|
-
focus;
|
|
262
|
+
focus; a running-row click attaches live while `␣` previews history.
|
|
263
|
+
Double-click or `Enter` opens there and transfers focus. The status bar
|
|
261
264
|
at the bottom-left shows the current layout and Target pane:
|
|
262
265
|
|
|
263
266
|
| Symbol | Meaning |
|
|
@@ -321,15 +324,22 @@ sessions move ahead of the other results.
|
|
|
321
324
|
## History preview
|
|
322
325
|
|
|
323
326
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
324
|
-
the Target pane without starting or resuming the agent.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
327
|
+
the Target pane without starting or resuming the agent. For a running session,
|
|
328
|
+
press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
|
|
329
|
+
read-only: it cannot send a message or change the session. User and assistant
|
|
330
|
+
messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
331
|
+
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
332
|
+
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
333
|
+
conversation: the retained prefix and replacement suffix remain visible, while
|
|
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.
|
|
328
337
|
|
|
329
338
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
330
339
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
331
|
-
matches, and `q` to exit
|
|
332
|
-
|
|
340
|
+
matches, and `q` to exit. If preview began from a live pane, `q` restores that
|
|
341
|
+
exact session; otherwise it restores the prior Target pane. Double-click to
|
|
342
|
+
skip preview and open the session directly.
|
|
333
343
|
|
|
334
344
|
## Status indicators
|
|
335
345
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.3.dev4"
|
|
@@ -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
|
|
@@ -157,6 +157,14 @@ class RootWheelForwardingManager:
|
|
|
157
157
|
state["backup"], token=token)
|
|
158
158
|
self._remove_state()
|
|
159
159
|
state = None
|
|
160
|
+
elif not tmux_ctl.root_wheel_bindings_are_current(token):
|
|
161
|
+
# The ownership transaction can survive a soft upgrade
|
|
162
|
+
# in the same tmux pane. Refresh only an exactly-owned
|
|
163
|
+
# wrapper so the new pane-local history route becomes
|
|
164
|
+
# active without treating it as a user binding.
|
|
165
|
+
if not tmux_ctl.set_root_wheel_forwarding(
|
|
166
|
+
state["backup"], token):
|
|
167
|
+
return False
|
|
160
168
|
if state is not None:
|
|
161
169
|
state["owners"] = sorted(
|
|
162
170
|
{*state["owners"], self._owner_pane_id})
|
|
@@ -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
|
|
|
@@ -1817,6 +1824,19 @@ _PREFIX_TARGET_KEY = "Tab"
|
|
|
1817
1824
|
_PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
1818
1825
|
RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
|
|
1819
1826
|
RAILMUX_TARGET_OPTION = "@railmux_target_pane"
|
|
1827
|
+
RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
|
|
1828
|
+
RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
|
|
1829
|
+
# tmux names function keys only through F12. Private controller routes use
|
|
1830
|
+
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1831
|
+
# user input forwarded to an agent pane.
|
|
1832
|
+
RAILMUX_HISTORY_PREVIEW_KEYS = {
|
|
1833
|
+
"primary": ("F21", "\x1b[35~"),
|
|
1834
|
+
"secondary": ("F22", "\x1b[36~"),
|
|
1835
|
+
}
|
|
1836
|
+
RAILMUX_HISTORY_RESTORE_KEYS = {
|
|
1837
|
+
"primary": ("F23", "\x1b[42~"),
|
|
1838
|
+
"secondary": ("F24", "\x1b[43~"),
|
|
1839
|
+
}
|
|
1820
1840
|
RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
|
|
1821
1841
|
RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
|
|
1822
1842
|
RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
|
|
@@ -2145,28 +2165,69 @@ def set_root_wheel_forwarding(
|
|
|
2145
2165
|
backup: RootWheelBindingBackup,
|
|
2146
2166
|
token: str,
|
|
2147
2167
|
) -> bool:
|
|
2148
|
-
"""
|
|
2168
|
+
"""Open canonical live history on wheel-up, otherwise preserve tmux input.
|
|
2149
2169
|
|
|
2150
2170
|
The literal marker makes crash recovery able to distinguish Railmux's
|
|
2151
2171
|
wrapper from a binding the user installed while Railmux was running.
|
|
2172
|
+
A pane-local primary/secondary marker routes the first live-agent wheel-up
|
|
2173
|
+
to Railmux's controller; the transcript viewer clears that marker, so its
|
|
2174
|
+
subsequent wheel events are forwarded normally.
|
|
2152
2175
|
Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
|
|
2153
2176
|
unbound by default and therefore has no false branch.
|
|
2154
2177
|
"""
|
|
2155
2178
|
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2156
|
-
|
|
2179
|
+
mouse_condition = (
|
|
2157
2180
|
"#{&&:#{mouse_any_flag},"
|
|
2158
2181
|
f"#{{==:{marker},{marker}}}}}"
|
|
2159
2182
|
)
|
|
2183
|
+
controller_present = f"#{{!=:#{{{RAILMUX_CONTROLLER_OPTION}}},}}"
|
|
2184
|
+
|
|
2185
|
+
def _preview_condition(slot_key: str) -> str:
|
|
2186
|
+
return (
|
|
2187
|
+
"#{&&:"
|
|
2188
|
+
f"#{{==:#{{{RAILMUX_HISTORY_PREVIEW_OPTION}}},{slot_key}}},"
|
|
2189
|
+
f"{controller_present}}}"
|
|
2190
|
+
)
|
|
2191
|
+
|
|
2192
|
+
def _send_preview(slot_key: str) -> str:
|
|
2193
|
+
_key, sequence = RAILMUX_HISTORY_PREVIEW_KEYS[slot_key]
|
|
2194
|
+
return (
|
|
2195
|
+
f"send-keys -l -t '#{{{RAILMUX_CONTROLLER_OPTION}}}' -- "
|
|
2196
|
+
f"'{sequence}'"
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2160
2199
|
try:
|
|
2161
2200
|
for key in _ROOT_WHEEL_KEYS:
|
|
2201
|
+
binding = backup.get(key)
|
|
2202
|
+
fallback = _binding_command(binding) if binding is not None else ""
|
|
2203
|
+
mouse_fallback = (
|
|
2204
|
+
f"if-shell -F -t = '{mouse_condition}' 'send-keys -M'"
|
|
2205
|
+
)
|
|
2206
|
+
if fallback:
|
|
2207
|
+
mouse_fallback += f" '{fallback}'"
|
|
2162
2208
|
args = [
|
|
2163
2209
|
"tmux", "bind-key", "-T", "root", key,
|
|
2164
|
-
"if-shell", "-F", "-t", "=", condition,
|
|
2165
|
-
"send-keys -M",
|
|
2166
2210
|
]
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2211
|
+
if key == "WheelUpPane":
|
|
2212
|
+
secondary_fallback = (
|
|
2213
|
+
"if-shell -F -t = "
|
|
2214
|
+
f"'{_preview_condition('secondary')}' "
|
|
2215
|
+
f"\"{_send_preview('secondary')}\" "
|
|
2216
|
+
f"\"{mouse_fallback}\""
|
|
2217
|
+
)
|
|
2218
|
+
args.extend([
|
|
2219
|
+
"if-shell", "-F", "-t", "=",
|
|
2220
|
+
_preview_condition("primary"),
|
|
2221
|
+
_send_preview("primary"),
|
|
2222
|
+
secondary_fallback,
|
|
2223
|
+
])
|
|
2224
|
+
else:
|
|
2225
|
+
args.extend([
|
|
2226
|
+
"if-shell", "-F", "-t", "=", mouse_condition,
|
|
2227
|
+
"send-keys -M",
|
|
2228
|
+
])
|
|
2229
|
+
if fallback:
|
|
2230
|
+
args.append(fallback)
|
|
2170
2231
|
subprocess.check_call(
|
|
2171
2232
|
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
2172
2233
|
except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
|
|
@@ -2186,6 +2247,28 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
|
|
|
2186
2247
|
)
|
|
2187
2248
|
|
|
2188
2249
|
|
|
2250
|
+
def root_wheel_bindings_are_current(token: str) -> bool:
|
|
2251
|
+
"""Whether the owned wrapper includes canonical-history routing."""
|
|
2252
|
+
bindings = read_root_wheel_bindings()
|
|
2253
|
+
up = bindings.get("WheelUpPane")
|
|
2254
|
+
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2255
|
+
return (
|
|
2256
|
+
all(
|
|
2257
|
+
binding is not None
|
|
2258
|
+
and marker in binding
|
|
2259
|
+
and "mouse_any_flag" in binding
|
|
2260
|
+
and "send-keys -M" in binding
|
|
2261
|
+
for binding in bindings.values()
|
|
2262
|
+
)
|
|
2263
|
+
and up is not None
|
|
2264
|
+
and RAILMUX_HISTORY_PREVIEW_OPTION in up
|
|
2265
|
+
and all(
|
|
2266
|
+
sequence in up or sequence.replace("\x1b", r"\033") in up
|
|
2267
|
+
for _key, sequence in RAILMUX_HISTORY_PREVIEW_KEYS.values()
|
|
2268
|
+
)
|
|
2269
|
+
)
|
|
2270
|
+
|
|
2271
|
+
|
|
2189
2272
|
def root_wheel_binding_is_original_or_owned(
|
|
2190
2273
|
key: str,
|
|
2191
2274
|
binding: str | None,
|
|
@@ -3272,6 +3355,47 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
|
|
|
3272
3355
|
)
|
|
3273
3356
|
|
|
3274
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
|
+
|
|
3275
3399
|
def kill_session_identity(identity: PaneIdentity) -> bool:
|
|
3276
3400
|
"""Kill an exact immutable tmux session, never a reused session name."""
|
|
3277
3401
|
current = pane_identity(identity.pane_id)
|