railmux 0.3.3.dev1__tar.gz → 0.3.3.dev3__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.dev1/src/railmux.egg-info → railmux-0.3.3.dev3}/PKG-INFO +19 -11
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/README.md +18 -10
- railmux-0.3.3.dev3/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/display_transport.py +18 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/mouse_manager.py +8 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_ctl.py +82 -7
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/transcript.py +83 -28
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/app.py +171 -15
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/modals.py +1 -1
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3/src/railmux.egg-info}/PKG-INFO +19 -11
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_display_transport.py +21 -1
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_focus_highlight.py +135 -8
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_mouse_manager.py +17 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_release_workflow.py +22 -8
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_soft_quit.py +81 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_ctl.py +28 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_integration.py +10 -2
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_transcript.py +84 -0
- railmux-0.3.3.dev1/src/railmux/__init__.py +0 -1
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/LICENSE +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/pyproject.toml +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/setup.cfg +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_fast_full_window.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_statusbar.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tools/release_notes.py +0 -0
- {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/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.dev3
|
|
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,20 @@ 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.
|
|
370
377
|
|
|
371
378
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
372
379
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
373
|
-
matches, and `q` to exit
|
|
374
|
-
|
|
380
|
+
matches, and `q` to exit. If preview began from a live pane, `q` restores that
|
|
381
|
+
exact session; otherwise it restores the prior Target pane. Double-click to
|
|
382
|
+
skip preview and open the session directly.
|
|
375
383
|
|
|
376
384
|
## Status indicators
|
|
377
385
|
|
|
@@ -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,20 @@ 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.
|
|
328
335
|
|
|
329
336
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
330
337
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
331
|
-
matches, and `q` to exit
|
|
332
|
-
|
|
338
|
+
matches, and `q` to exit. If preview began from a live pane, `q` restores that
|
|
339
|
+
exact session; otherwise it restores the prior Target pane. Double-click to
|
|
340
|
+
skip preview and open the session directly.
|
|
333
341
|
|
|
334
342
|
## Status indicators
|
|
335
343
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.3.dev3"
|
|
@@ -738,6 +738,22 @@ class AgentDisplayTransport:
|
|
|
738
738
|
def _with_phase(state: SwapState, phase: str) -> SwapState:
|
|
739
739
|
return SwapState(**{**asdict(state), "phase": phase})
|
|
740
740
|
|
|
741
|
+
@staticmethod
|
|
742
|
+
def _fit_home_to_display(state: SwapState) -> None:
|
|
743
|
+
"""Best-effort preserve the provider's visible geometry at home.
|
|
744
|
+
|
|
745
|
+
While a real pane is displayed, its named session contains only the
|
|
746
|
+
inert placeholder. Resize that detached home window to the real
|
|
747
|
+
pane's current size before swapping back. Otherwise background output
|
|
748
|
+
is formatted at the placeholder's older (often much narrower) launch
|
|
749
|
+
geometry and later appears as a narrow discontinuity in scrollback.
|
|
750
|
+
|
|
751
|
+
Geometry is presentation-only: failure must never weaken or abort the
|
|
752
|
+
identity-pinned return transaction.
|
|
753
|
+
"""
|
|
754
|
+
tmux_ctl.fit_session_to_pane(
|
|
755
|
+
state.agent_tmux_name, state.agent_pane_id)
|
|
756
|
+
|
|
741
757
|
def park(self, slot: AgentSlot) -> bool:
|
|
742
758
|
"""Move one displayed swap agent home without forgetting its slot.
|
|
743
759
|
|
|
@@ -764,6 +780,7 @@ class AgentDisplayTransport:
|
|
|
764
780
|
parking = self._with_phase(state, "parking")
|
|
765
781
|
if not _write_marker_pair(parking, slot.key):
|
|
766
782
|
return False
|
|
783
|
+
self._fit_home_to_display(state)
|
|
767
784
|
if not tmux_ctl.swap_panes(
|
|
768
785
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
769
786
|
_write_marker_pair(state, slot.key)
|
|
@@ -857,6 +874,7 @@ class AgentDisplayTransport:
|
|
|
857
874
|
return False
|
|
858
875
|
returning = SwapState(**{**asdict(state), "phase": "returning"})
|
|
859
876
|
_write_marker_pair(returning, slot.key)
|
|
877
|
+
self._fit_home_to_display(state)
|
|
860
878
|
if not tmux_ctl.swap_panes(
|
|
861
879
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
862
880
|
return False
|
|
@@ -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})
|
|
@@ -1817,6 +1817,18 @@ _PREFIX_TARGET_KEY = "Tab"
|
|
|
1817
1817
|
_PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
1818
1818
|
RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
|
|
1819
1819
|
RAILMUX_TARGET_OPTION = "@railmux_target_pane"
|
|
1820
|
+
RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
|
|
1821
|
+
# tmux names function keys only through F12. Private controller routes use
|
|
1822
|
+
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1823
|
+
# user input forwarded to an agent pane.
|
|
1824
|
+
RAILMUX_HISTORY_PREVIEW_KEYS = {
|
|
1825
|
+
"primary": ("F21", "\x1b[35~"),
|
|
1826
|
+
"secondary": ("F22", "\x1b[36~"),
|
|
1827
|
+
}
|
|
1828
|
+
RAILMUX_HISTORY_RESTORE_KEYS = {
|
|
1829
|
+
"primary": ("F23", "\x1b[42~"),
|
|
1830
|
+
"secondary": ("F24", "\x1b[43~"),
|
|
1831
|
+
}
|
|
1820
1832
|
RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
|
|
1821
1833
|
RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
|
|
1822
1834
|
RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
|
|
@@ -2145,28 +2157,69 @@ def set_root_wheel_forwarding(
|
|
|
2145
2157
|
backup: RootWheelBindingBackup,
|
|
2146
2158
|
token: str,
|
|
2147
2159
|
) -> bool:
|
|
2148
|
-
"""
|
|
2160
|
+
"""Open canonical live history on wheel-up, otherwise preserve tmux input.
|
|
2149
2161
|
|
|
2150
2162
|
The literal marker makes crash recovery able to distinguish Railmux's
|
|
2151
2163
|
wrapper from a binding the user installed while Railmux was running.
|
|
2164
|
+
A pane-local primary/secondary marker routes the first live-agent wheel-up
|
|
2165
|
+
to Railmux's controller; the transcript viewer clears that marker, so its
|
|
2166
|
+
subsequent wheel events are forwarded normally.
|
|
2152
2167
|
Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
|
|
2153
2168
|
unbound by default and therefore has no false branch.
|
|
2154
2169
|
"""
|
|
2155
2170
|
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2156
|
-
|
|
2171
|
+
mouse_condition = (
|
|
2157
2172
|
"#{&&:#{mouse_any_flag},"
|
|
2158
2173
|
f"#{{==:{marker},{marker}}}}}"
|
|
2159
2174
|
)
|
|
2175
|
+
controller_present = f"#{{!=:#{{{RAILMUX_CONTROLLER_OPTION}}},}}"
|
|
2176
|
+
|
|
2177
|
+
def _preview_condition(slot_key: str) -> str:
|
|
2178
|
+
return (
|
|
2179
|
+
"#{&&:"
|
|
2180
|
+
f"#{{==:#{{{RAILMUX_HISTORY_PREVIEW_OPTION}}},{slot_key}}},"
|
|
2181
|
+
f"{controller_present}}}"
|
|
2182
|
+
)
|
|
2183
|
+
|
|
2184
|
+
def _send_preview(slot_key: str) -> str:
|
|
2185
|
+
_key, sequence = RAILMUX_HISTORY_PREVIEW_KEYS[slot_key]
|
|
2186
|
+
return (
|
|
2187
|
+
f"send-keys -l -t '#{{{RAILMUX_CONTROLLER_OPTION}}}' -- "
|
|
2188
|
+
f"'{sequence}'"
|
|
2189
|
+
)
|
|
2190
|
+
|
|
2160
2191
|
try:
|
|
2161
2192
|
for key in _ROOT_WHEEL_KEYS:
|
|
2193
|
+
binding = backup.get(key)
|
|
2194
|
+
fallback = _binding_command(binding) if binding is not None else ""
|
|
2195
|
+
mouse_fallback = (
|
|
2196
|
+
f"if-shell -F -t = '{mouse_condition}' 'send-keys -M'"
|
|
2197
|
+
)
|
|
2198
|
+
if fallback:
|
|
2199
|
+
mouse_fallback += f" '{fallback}'"
|
|
2162
2200
|
args = [
|
|
2163
2201
|
"tmux", "bind-key", "-T", "root", key,
|
|
2164
|
-
"if-shell", "-F", "-t", "=", condition,
|
|
2165
|
-
"send-keys -M",
|
|
2166
2202
|
]
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2203
|
+
if key == "WheelUpPane":
|
|
2204
|
+
secondary_fallback = (
|
|
2205
|
+
"if-shell -F -t = "
|
|
2206
|
+
f"'{_preview_condition('secondary')}' "
|
|
2207
|
+
f"\"{_send_preview('secondary')}\" "
|
|
2208
|
+
f"\"{mouse_fallback}\""
|
|
2209
|
+
)
|
|
2210
|
+
args.extend([
|
|
2211
|
+
"if-shell", "-F", "-t", "=",
|
|
2212
|
+
_preview_condition("primary"),
|
|
2213
|
+
_send_preview("primary"),
|
|
2214
|
+
secondary_fallback,
|
|
2215
|
+
])
|
|
2216
|
+
else:
|
|
2217
|
+
args.extend([
|
|
2218
|
+
"if-shell", "-F", "-t", "=", mouse_condition,
|
|
2219
|
+
"send-keys -M",
|
|
2220
|
+
])
|
|
2221
|
+
if fallback:
|
|
2222
|
+
args.append(fallback)
|
|
2170
2223
|
subprocess.check_call(
|
|
2171
2224
|
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
2172
2225
|
except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
|
|
@@ -2186,6 +2239,28 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
|
|
|
2186
2239
|
)
|
|
2187
2240
|
|
|
2188
2241
|
|
|
2242
|
+
def root_wheel_bindings_are_current(token: str) -> bool:
|
|
2243
|
+
"""Whether the owned wrapper includes canonical-history routing."""
|
|
2244
|
+
bindings = read_root_wheel_bindings()
|
|
2245
|
+
up = bindings.get("WheelUpPane")
|
|
2246
|
+
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2247
|
+
return (
|
|
2248
|
+
all(
|
|
2249
|
+
binding is not None
|
|
2250
|
+
and marker in binding
|
|
2251
|
+
and "mouse_any_flag" in binding
|
|
2252
|
+
and "send-keys -M" in binding
|
|
2253
|
+
for binding in bindings.values()
|
|
2254
|
+
)
|
|
2255
|
+
and up is not None
|
|
2256
|
+
and RAILMUX_HISTORY_PREVIEW_OPTION in up
|
|
2257
|
+
and all(
|
|
2258
|
+
sequence in up or sequence.replace("\x1b", r"\033") in up
|
|
2259
|
+
for _key, sequence in RAILMUX_HISTORY_PREVIEW_KEYS.values()
|
|
2260
|
+
)
|
|
2261
|
+
)
|
|
2262
|
+
|
|
2263
|
+
|
|
2189
2264
|
def root_wheel_binding_is_original_or_owned(
|
|
2190
2265
|
key: str,
|
|
2191
2266
|
binding: str | None,
|
|
@@ -295,6 +295,81 @@ def _render_claude_native_tool_results(
|
|
|
295
295
|
yield RESET
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
def _claude_current_branch(records: list[dict]) -> list[dict]:
|
|
299
|
+
"""Project an append-only Claude JSONL onto its current main branch.
|
|
300
|
+
|
|
301
|
+
Claude Code links semantic records with ``uuid`` / ``parentUuid``. Editing
|
|
302
|
+
or rewinding a prompt appends a second child instead of removing the old
|
|
303
|
+
suffix, so file order is not conversation order. The newest non-sidechain
|
|
304
|
+
UUID is the active tip; walking its parents retains the shared prefix and
|
|
305
|
+
the replacement suffix while excluding abandoned and sub-agent branches.
|
|
306
|
+
|
|
307
|
+
UUID-less metadata is retained for the normal renderer filters. If the
|
|
308
|
+
input is a bounded tail, walking stops safely at the first missing parent.
|
|
309
|
+
"""
|
|
310
|
+
by_uuid: dict[str, dict] = {}
|
|
311
|
+
tip: str | None = None
|
|
312
|
+
for record in records:
|
|
313
|
+
uuid = record.get("uuid")
|
|
314
|
+
if not isinstance(uuid, str) or not uuid:
|
|
315
|
+
continue
|
|
316
|
+
by_uuid[uuid] = record
|
|
317
|
+
if record.get("isSidechain") is not True:
|
|
318
|
+
tip = uuid
|
|
319
|
+
if tip is None:
|
|
320
|
+
return records
|
|
321
|
+
|
|
322
|
+
current: set[str] = set()
|
|
323
|
+
cursor: str | None = tip
|
|
324
|
+
while cursor and cursor not in current:
|
|
325
|
+
record = by_uuid.get(cursor)
|
|
326
|
+
if record is None:
|
|
327
|
+
break
|
|
328
|
+
current.add(cursor)
|
|
329
|
+
parent = record.get("parentUuid")
|
|
330
|
+
cursor = parent if isinstance(parent, str) and parent else None
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
record for record in records
|
|
334
|
+
if not isinstance(record.get("uuid"), str)
|
|
335
|
+
or not record.get("uuid")
|
|
336
|
+
or record["uuid"] in current
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _render_claude_records(
|
|
341
|
+
records: list[dict], *, claude_native: bool,
|
|
342
|
+
):
|
|
343
|
+
"""Render only the active Claude branch from buffered JSONL records."""
|
|
344
|
+
calls: dict[str, str] = {}
|
|
345
|
+
for record in _claude_current_branch(records):
|
|
346
|
+
rtype = record.get("type", "")
|
|
347
|
+
if rtype in _SKIP_TYPES:
|
|
348
|
+
continue
|
|
349
|
+
if _is_real_user(record):
|
|
350
|
+
rendered = (
|
|
351
|
+
_render_claude_native_user(record)
|
|
352
|
+
if claude_native
|
|
353
|
+
else _render_user(record)
|
|
354
|
+
)
|
|
355
|
+
if rendered:
|
|
356
|
+
yield rendered
|
|
357
|
+
if rtype == "user":
|
|
358
|
+
renderer = (
|
|
359
|
+
_render_claude_native_tool_results
|
|
360
|
+
if claude_native
|
|
361
|
+
else _render_claude_tool_results
|
|
362
|
+
)
|
|
363
|
+
yield from renderer(record, calls)
|
|
364
|
+
elif rtype == "assistant":
|
|
365
|
+
renderer = (
|
|
366
|
+
_render_claude_native_assistant
|
|
367
|
+
if claude_native
|
|
368
|
+
else _render_assistant_blocks
|
|
369
|
+
)
|
|
370
|
+
yield from renderer(record, calls)
|
|
371
|
+
|
|
372
|
+
|
|
298
373
|
def format_transcript(
|
|
299
374
|
source: Path | object,
|
|
300
375
|
fmt: str | None = None,
|
|
@@ -326,7 +401,10 @@ def format_transcript(
|
|
|
326
401
|
# call_id -> tool name, so Codex tool outputs can be labelled with the
|
|
327
402
|
# call that produced them (issue #3).
|
|
328
403
|
codex_calls: dict[str, str] = {}
|
|
329
|
-
|
|
404
|
+
# Claude rewind/edit branches live in one append-only JSONL, so those
|
|
405
|
+
# records must be buffered until the active tip is known. Production
|
|
406
|
+
# callers bound the input to the latest 2,000 records.
|
|
407
|
+
claude_records: list[dict] = []
|
|
330
408
|
try:
|
|
331
409
|
if isinstance(source, Path):
|
|
332
410
|
fh = open(source, "r", encoding="utf-8")
|
|
@@ -360,33 +438,10 @@ def format_transcript(
|
|
|
360
438
|
if _codex:
|
|
361
439
|
yield from _render_codex(record, codex_calls)
|
|
362
440
|
continue
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if _is_real_user(record):
|
|
369
|
-
rendered = (
|
|
370
|
-
_render_claude_native_user(record)
|
|
371
|
-
if claude_native
|
|
372
|
-
else _render_user(record)
|
|
373
|
-
)
|
|
374
|
-
if rendered:
|
|
375
|
-
yield rendered
|
|
376
|
-
if rtype == "user":
|
|
377
|
-
renderer = (
|
|
378
|
-
_render_claude_native_tool_results
|
|
379
|
-
if claude_native
|
|
380
|
-
else _render_claude_tool_results
|
|
381
|
-
)
|
|
382
|
-
yield from renderer(record, claude_calls)
|
|
383
|
-
elif rtype == "assistant":
|
|
384
|
-
renderer = (
|
|
385
|
-
_render_claude_native_assistant
|
|
386
|
-
if claude_native
|
|
387
|
-
else _render_assistant_blocks
|
|
388
|
-
)
|
|
389
|
-
yield from renderer(record, claude_calls)
|
|
441
|
+
claude_records.append(record)
|
|
442
|
+
if _codex is False:
|
|
443
|
+
yield from _render_claude_records(
|
|
444
|
+
claude_records, claude_native=claude_native)
|
|
390
445
|
except OSError:
|
|
391
446
|
yield f"{YELLOW}Could not read: {source}{RESET}\n"
|
|
392
447
|
finally:
|