railmux 0.3.3.dev2__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.dev2/src/railmux.egg-info → railmux-0.3.3.dev3}/PKG-INFO +19 -11
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/README.md +18 -10
- railmux-0.3.3.dev3/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/mouse_manager.py +8 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/tmux_ctl.py +82 -7
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/transcript.py +83 -28
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/app.py +141 -15
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/modals.py +1 -1
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3/src/railmux.egg-info}/PKG-INFO +19 -11
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_focus_highlight.py +135 -8
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_mouse_manager.py +17 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_release_workflow.py +22 -8
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_soft_quit.py +42 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tmux_ctl.py +28 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tmux_integration.py +1 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_transcript.py +84 -0
- railmux-0.3.3.dev2/src/railmux/__init__.py +0 -1
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/LICENSE +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/pyproject.toml +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/setup.cfg +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_display_transport.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_fast_full_window.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_statusbar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.3.3.dev3}/tools/release_notes.py +0 -0
- {railmux-0.3.3.dev2 → 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"
|
|
@@ -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:
|
|
@@ -1072,7 +1072,7 @@ class App:
|
|
|
1072
1072
|
session_id: str | None,
|
|
1073
1073
|
tmux_name: str | None,
|
|
1074
1074
|
) -> None:
|
|
1075
|
-
"""Stamp
|
|
1075
|
+
"""Stamp transcript/history routing for the displayed live agent."""
|
|
1076
1076
|
pane_id = slot.pane_id
|
|
1077
1077
|
if pane_id is None or not pane_id.startswith("%"):
|
|
1078
1078
|
return
|
|
@@ -1098,6 +1098,19 @@ class App:
|
|
|
1098
1098
|
tmux_server.TRANSCRIPT_SOURCE_OPTION,
|
|
1099
1099
|
marker,
|
|
1100
1100
|
)
|
|
1101
|
+
history_slot = None
|
|
1102
|
+
if (
|
|
1103
|
+
running is not None
|
|
1104
|
+
and running.session_type in ("claude", "codex")
|
|
1105
|
+
and logical_id is not None
|
|
1106
|
+
and not running.is_placeholder
|
|
1107
|
+
):
|
|
1108
|
+
history_slot = slot.key
|
|
1109
|
+
tmux_ctl.set_pane_user_option(
|
|
1110
|
+
pane_id,
|
|
1111
|
+
tmux_ctl.RAILMUX_HISTORY_PREVIEW_OPTION,
|
|
1112
|
+
history_slot,
|
|
1113
|
+
)
|
|
1101
1114
|
|
|
1102
1115
|
def _paint_slot_active_target(
|
|
1103
1116
|
self,
|
|
@@ -1900,11 +1913,16 @@ class App:
|
|
|
1900
1913
|
self._on_session_preview(session)
|
|
1901
1914
|
|
|
1902
1915
|
def _on_session_preview(self, session: SessionMeta) -> None:
|
|
1903
|
-
"""Show
|
|
1916
|
+
"""Show the provider's canonical current-branch history.
|
|
1904
1917
|
|
|
1905
|
-
Stopped-row clicks, Space,
|
|
1906
|
-
|
|
1918
|
+
Stopped-row clicks, Space, context Preview, and live-pane wheel-up use
|
|
1919
|
+
this read-only projection. A Codex rewind lineage is represented by its
|
|
1920
|
+
newest rollout; Claude branches are projected by ``transcript``.
|
|
1907
1921
|
"""
|
|
1922
|
+
if session.session_type == "codex":
|
|
1923
|
+
representative = self._codex_representative(session.session_id)
|
|
1924
|
+
if representative is not None:
|
|
1925
|
+
session = representative
|
|
1908
1926
|
slot = self._agent_workspace().target
|
|
1909
1927
|
self._cancel_pending_double_focus()
|
|
1910
1928
|
if not self._has_less:
|
|
@@ -1939,7 +1957,65 @@ class App:
|
|
|
1939
1957
|
slot, session.session_id, None, **target_kwargs)
|
|
1940
1958
|
if not self._show_attention_status(session.attention):
|
|
1941
1959
|
self._set_status(
|
|
1942
|
-
f"≡ Previewing {session.display_title} (history)")
|
|
1960
|
+
f"≡ Previewing {session.display_title} (current history)")
|
|
1961
|
+
|
|
1962
|
+
def _preview_running_history(self, entry: RunningEntry) -> None:
|
|
1963
|
+
"""Preview the exact live entry's canonical provider history."""
|
|
1964
|
+
running = self._by_tmux(entry.tmux_name)
|
|
1965
|
+
if (running is None or running.is_placeholder
|
|
1966
|
+
or not self._agent_session_alive(entry.tmux_name)):
|
|
1967
|
+
self._set_status("Live history is no longer available.", "warn")
|
|
1968
|
+
return
|
|
1969
|
+
session_id = running.logical_session_id
|
|
1970
|
+
if session_id is None:
|
|
1971
|
+
self._set_status("Waiting for the provider history file…", "tip")
|
|
1972
|
+
return
|
|
1973
|
+
session = self._find_session_meta(
|
|
1974
|
+
session_id, running.project, running.session_type)
|
|
1975
|
+
if session is None:
|
|
1976
|
+
self._set_status("Waiting for the provider history file…", "tip")
|
|
1977
|
+
return
|
|
1978
|
+
slot = (self._agent_workspace().slot_for_agent(entry.tmux_name)
|
|
1979
|
+
or self._agent_workspace().target)
|
|
1980
|
+
self._set_workspace_target(slot.key)
|
|
1981
|
+
self._on_session_preview(session)
|
|
1982
|
+
|
|
1983
|
+
def _preview_slot_history(self, slot: AgentSlot) -> None:
|
|
1984
|
+
"""Handle a pane-local wheel-up routed through the controller."""
|
|
1985
|
+
tmux_name = slot.agent_tmux_name
|
|
1986
|
+
if tmux_name is None:
|
|
1987
|
+
return
|
|
1988
|
+
running = self._by_tmux(tmux_name)
|
|
1989
|
+
if running is None:
|
|
1990
|
+
return
|
|
1991
|
+
self._preview_running_history(RunningEntry(
|
|
1992
|
+
tmux_name=tmux_name,
|
|
1993
|
+
label=running.label,
|
|
1994
|
+
status=running.status,
|
|
1995
|
+
))
|
|
1996
|
+
|
|
1997
|
+
def _restore_history_slot(self, slot: AgentSlot) -> None:
|
|
1998
|
+
"""Return a closed live-history viewer to the exact saved agent."""
|
|
1999
|
+
if not slot.in_history_mode:
|
|
2000
|
+
return
|
|
2001
|
+
restore = slot.restore_state
|
|
2002
|
+
if (restore is None or restore.kind != "agent"
|
|
2003
|
+
or restore.tmux_name is None):
|
|
2004
|
+
return
|
|
2005
|
+
running = self._by_tmux(restore.tmux_name)
|
|
2006
|
+
if (running is None
|
|
2007
|
+
or not self._agent_session_alive(restore.tmux_name)):
|
|
2008
|
+
self._set_status("The previewed live session has ended.", "warn")
|
|
2009
|
+
return
|
|
2010
|
+
self._on_running_select(
|
|
2011
|
+
RunningEntry(
|
|
2012
|
+
tmux_name=running.tmux_name,
|
|
2013
|
+
label=running.label,
|
|
2014
|
+
status=running.status,
|
|
2015
|
+
),
|
|
2016
|
+
steal_focus=False,
|
|
2017
|
+
slot=slot,
|
|
2018
|
+
)
|
|
1943
2019
|
|
|
1944
2020
|
def _save_restore_state(self, slot: AgentSlot | None = None) -> None:
|
|
1945
2021
|
"""Remember what's in the right pane before taking it over for history."""
|
|
@@ -1981,6 +2057,7 @@ class App:
|
|
|
1981
2057
|
import shlex
|
|
1982
2058
|
import sys as _sys
|
|
1983
2059
|
mouse = self._less_mouse_flag
|
|
2060
|
+
slot = slot or self._agent_workspace().target
|
|
1984
2061
|
# Tail the last 2000 lines so large sessions appear instantly. Tailing
|
|
1985
2062
|
# drops the leading ``session_meta`` record, so a long Codex rollout
|
|
1986
2063
|
# would otherwise be auto-detected as Claude and render blank. Pass the
|
|
@@ -1998,7 +2075,17 @@ class App:
|
|
|
1998
2075
|
f"{python} -m railmux.transcript --format {fmt} "
|
|
1999
2076
|
f"--preview-limit 2000 - | "
|
|
2000
2077
|
f"{less_env} less -R +G {mouse}").rstrip()
|
|
2001
|
-
|
|
2078
|
+
restore = slot.restore_state
|
|
2079
|
+
controller = getattr(self, "_railmux_pane_id", None)
|
|
2080
|
+
if (restore is not None and restore.kind == "agent" and controller
|
|
2081
|
+
and slot.key in tmux_ctl.RAILMUX_HISTORY_RESTORE_KEYS):
|
|
2082
|
+
_restore_key, restore_sequence = (
|
|
2083
|
+
tmux_ctl.RAILMUX_HISTORY_RESTORE_KEYS[slot.key])
|
|
2084
|
+
cmd += (
|
|
2085
|
+
"; tmux send-keys -l -t "
|
|
2086
|
+
f"{shlex.quote(controller)} -- "
|
|
2087
|
+
f"{shlex.quote(restore_sequence)}"
|
|
2088
|
+
)
|
|
2002
2089
|
# In swap mode ``slot.pane_id`` is the real provider pane. Return it
|
|
2003
2090
|
# home before the destructive ``respawn-pane -k`` below; only the
|
|
2004
2091
|
# display placeholder may be replaced by the transcript viewer.
|
|
@@ -3697,7 +3784,7 @@ class App:
|
|
|
3697
3784
|
return None
|
|
3698
3785
|
|
|
3699
3786
|
def _preview_focused_target(self) -> None:
|
|
3700
|
-
"""
|
|
3787
|
+
"""Preview canonical history for the focused session or live entry."""
|
|
3701
3788
|
target = self._focused_pane_menu_target()
|
|
3702
3789
|
if target is None:
|
|
3703
3790
|
self._set_status(
|
|
@@ -3706,9 +3793,9 @@ class App:
|
|
|
3706
3793
|
return
|
|
3707
3794
|
kind, value, _label = target
|
|
3708
3795
|
if kind == "session" and isinstance(value, SessionMeta):
|
|
3709
|
-
self.
|
|
3796
|
+
self._on_session_preview(value)
|
|
3710
3797
|
elif kind == "running" and isinstance(value, RunningEntry):
|
|
3711
|
-
self.
|
|
3798
|
+
self._preview_running_history(value)
|
|
3712
3799
|
|
|
3713
3800
|
def _close_secondary_split(self, *, announce: bool = True) -> bool:
|
|
3714
3801
|
workspace = self._agent_workspace()
|
|
@@ -7313,6 +7400,27 @@ class App:
|
|
|
7313
7400
|
if key == COMPACT_RESIZE_KEY.lower():
|
|
7314
7401
|
self._handle_remote_compact_prepare()
|
|
7315
7402
|
return
|
|
7403
|
+
for slot_key, (preview_key, _sequence) in (
|
|
7404
|
+
tmux_ctl.RAILMUX_HISTORY_PREVIEW_KEYS.items()):
|
|
7405
|
+
if key == preview_key.lower():
|
|
7406
|
+
if (getattr(self, "_loop", None) is not None
|
|
7407
|
+
and self._loop.widget is not self._frame):
|
|
7408
|
+
return
|
|
7409
|
+
workspace = self._agent_workspace()
|
|
7410
|
+
self._preview_slot_history(
|
|
7411
|
+
workspace.primary
|
|
7412
|
+
if slot_key == AgentWorkspace.PRIMARY
|
|
7413
|
+
else workspace.secondary)
|
|
7414
|
+
return
|
|
7415
|
+
for slot_key, (restore_key, _sequence) in (
|
|
7416
|
+
tmux_ctl.RAILMUX_HISTORY_RESTORE_KEYS.items()):
|
|
7417
|
+
if key == restore_key.lower():
|
|
7418
|
+
workspace = self._agent_workspace()
|
|
7419
|
+
self._restore_history_slot(
|
|
7420
|
+
workspace.primary
|
|
7421
|
+
if slot_key == AgentWorkspace.PRIMARY
|
|
7422
|
+
else workspace.secondary)
|
|
7423
|
+
return
|
|
7316
7424
|
# F9 is routed here by a global tmux binding and must remain available
|
|
7317
7425
|
# while a modal is open (notably Help's fullscreen copy workflow).
|
|
7318
7426
|
if key == "f9":
|
|
@@ -8518,7 +8626,7 @@ class App:
|
|
|
8518
8626
|
# Index visible projects by real_path. A Claude placeholder becomes
|
|
8519
8627
|
# resolvable once its first real session makes the project visible;
|
|
8520
8628
|
# Codex New Project can use its in-memory synthetic project earlier.
|
|
8521
|
-
by_path = {p.real_path: p for p in projects}
|
|
8629
|
+
by_path = {self._path_key(p.real_path): p for p in projects}
|
|
8522
8630
|
claimed = self._running_session_ids() | set(self._running)
|
|
8523
8631
|
resolved_any = False
|
|
8524
8632
|
for r in placeholders:
|
|
@@ -8526,7 +8634,11 @@ class App:
|
|
|
8526
8634
|
# first rollout makes that cwd visible in the Codex index. Claude
|
|
8527
8635
|
# placeholders continue to wait for discovery to supply the real
|
|
8528
8636
|
# encoded project directory.
|
|
8529
|
-
|
|
8637
|
+
placeholder_key = (
|
|
8638
|
+
self._path_key(r.placeholder_path)
|
|
8639
|
+
if r.placeholder_path is not None else None
|
|
8640
|
+
)
|
|
8641
|
+
project = by_path.get(placeholder_key) or r.project
|
|
8530
8642
|
if project is None:
|
|
8531
8643
|
continue
|
|
8532
8644
|
registered_mode = self._modes().for_tmux_name(r.tmux_name)
|
|
@@ -8599,11 +8711,25 @@ class App:
|
|
|
8599
8711
|
if not self._write_orphan_marker(resolved_marker):
|
|
8600
8712
|
continue
|
|
8601
8713
|
r.orphan = resolved_marker
|
|
8714
|
+
# Codex metadata always carries its synthetic ``-cx-`` project
|
|
8715
|
+
# key, while the Projects pane reuses a discovered Claude project
|
|
8716
|
+
# object when the same cwd exists in both providers. Preserve the
|
|
8717
|
+
# currently-published project identity across promotion: replacing
|
|
8718
|
+
# it with the metadata object for one tick makes the later
|
|
8719
|
+
# encoded-name match fail, briefly clearing Sessions to its
|
|
8720
|
+
# "Select a project" state before path-based selection recovers.
|
|
8721
|
+
promoted_project = (
|
|
8722
|
+
by_path.get(self._path_key(candidate.project.real_path))
|
|
8723
|
+
or project
|
|
8724
|
+
or candidate.project
|
|
8725
|
+
)
|
|
8602
8726
|
# Re-key the entry from the placeholder to the real session_id.
|
|
8603
8727
|
del self._running[r.key]
|
|
8604
8728
|
r.key = candidate.session_id
|
|
8605
|
-
r.label =
|
|
8606
|
-
|
|
8729
|
+
r.label = (
|
|
8730
|
+
f"{promoted_project.display_name}/{candidate.display_title}"
|
|
8731
|
+
)
|
|
8732
|
+
r.project = promoted_project
|
|
8607
8733
|
r.placeholder_path = None
|
|
8608
8734
|
r.created_at = 0.0
|
|
8609
8735
|
# Publish the same provider generation atomically to Running. The
|
|
@@ -8622,7 +8748,7 @@ class App:
|
|
|
8622
8748
|
self._set_slot_active_target(
|
|
8623
8749
|
displayed_slot, candidate.session_id, r.tmux_name)
|
|
8624
8750
|
if displayed_slot is self._agent_workspace().target:
|
|
8625
|
-
self._set_current_project(
|
|
8751
|
+
self._set_current_project(promoted_project)
|
|
8626
8752
|
if resolved_any:
|
|
8627
8753
|
# Placeholder launch time may already have consumed the ordinary
|
|
8628
8754
|
# one-minute sort budget. Its first authoritative provider
|
|
@@ -9302,7 +9428,7 @@ class App:
|
|
|
9302
9428
|
(_context_menu_label("Open", "↵"),
|
|
9303
9429
|
lambda s=session: self._do_context_open(s)),
|
|
9304
9430
|
(_context_menu_label("Preview", "␣"), lambda s=session:
|
|
9305
|
-
self.
|
|
9431
|
+
self._on_session_preview(s)),
|
|
9306
9432
|
(_context_menu_label("Info", "i"),
|
|
9307
9433
|
lambda s=session: self._do_context_info(s)),
|
|
9308
9434
|
(_context_menu_label("Rename", "r"),
|