railmux 0.3.3.dev5__tar.gz → 0.3.4.dev1__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.dev5/src/railmux.egg-info → railmux-0.3.4.dev1}/PKG-INFO +20 -13
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/README.md +19 -12
- railmux-0.3.4.dev1/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_server.py +45 -10
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_ctl.py +42 -69
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_server.py +22 -4
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/app.py +95 -65
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1/src/railmux.egg-info}/PKG-INFO +20 -13
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_delete.py +1 -1
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_lineage.py +108 -12
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_fast_full_window.py +158 -1
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_focus_highlight.py +25 -15
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_statusbar.py +12 -3
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_ctl.py +28 -17
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_integration.py +14 -6
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_server.py +22 -0
- railmux-0.3.3.dev5/src/railmux/__init__.py +0 -1
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/LICENSE +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/pyproject.toml +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/setup.cfg +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/transcript.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_display_transport.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_transcript.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tools/release_notes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/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
|
+
Version: 0.3.4.dev1
|
|
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,7 +169,7 @@ 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
|
|
172
|
+
| Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
|
|
173
173
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
174
174
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
175
175
|
|
|
@@ -243,8 +243,9 @@ The rename popup starts with the current title pre-filled. Press
|
|
|
243
243
|
to cancel.
|
|
244
244
|
|
|
245
245
|
After a delete is confirmed, its popup closes immediately. The bottom-right
|
|
246
|
-
status shows **Deleting
|
|
247
|
-
removes its history in the background, then reports the
|
|
246
|
+
status shows **Deleting “session name”…** while Railmux stops the exact
|
|
247
|
+
provider session and removes its history in the background, then reports the
|
|
248
|
+
final result.
|
|
248
249
|
|
|
249
250
|
Press `/` in Projects, Sessions, or Running to filter that section. Active
|
|
250
251
|
filters are marked with `[filtered]` and survive a soft restart; reopening `/`
|
|
@@ -371,15 +372,21 @@ sessions move ahead of the other results.
|
|
|
371
372
|
|
|
372
373
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
373
374
|
the Target pane without starting or resuming the agent. For a running session,
|
|
374
|
-
press
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
tmux
|
|
375
|
+
press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
|
|
376
|
+
or change the session. User and assistant messages, tool calls, and abbreviated
|
|
377
|
+
tool output are colour-coded, while internal context and encrypted reasoning
|
|
378
|
+
are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
|
|
379
|
+
projected onto the provider's current conversation: the retained prefix and
|
|
380
|
+
replacement suffix remain visible, while the abandoned suffix is hidden.
|
|
381
|
+
|
|
382
|
+
Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
383
|
+
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
384
|
+
per-pane history and normally preserves the live terminal's captured styling.
|
|
385
|
+
When Railmux confirms that a running Codex session advanced after rewind or
|
|
386
|
+
steering, it resets only the current terminal view before Codex redraws and
|
|
387
|
+
invalidates that pane's SSH history generation. Retained tmux scrollback is not
|
|
388
|
+
erased; the SSH history for that branched generation uses the canonical
|
|
389
|
+
rollout so its abandoned suffix stays hidden.
|
|
383
390
|
|
|
384
391
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
385
392
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -127,7 +127,7 @@ for focus and layout limits.
|
|
|
127
127
|
|--------|--------|
|
|
128
128
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
129
129
|
| Left-click (running) | Switch the Target pane to that session |
|
|
130
|
-
| Wheel
|
|
130
|
+
| Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
|
|
131
131
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
132
132
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
133
133
|
|
|
@@ -201,8 +201,9 @@ The rename popup starts with the current title pre-filled. Press
|
|
|
201
201
|
to cancel.
|
|
202
202
|
|
|
203
203
|
After a delete is confirmed, its popup closes immediately. The bottom-right
|
|
204
|
-
status shows **Deleting
|
|
205
|
-
removes its history in the background, then reports the
|
|
204
|
+
status shows **Deleting “session name”…** while Railmux stops the exact
|
|
205
|
+
provider session and removes its history in the background, then reports the
|
|
206
|
+
final result.
|
|
206
207
|
|
|
207
208
|
Press `/` in Projects, Sessions, or Running to filter that section. Active
|
|
208
209
|
filters are marked with `[filtered]` and survive a soft restart; reopening `/`
|
|
@@ -329,15 +330,21 @@ sessions move ahead of the other results.
|
|
|
329
330
|
|
|
330
331
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
331
332
|
the Target pane without starting or resuming the agent. For a running session,
|
|
332
|
-
press
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
tmux
|
|
333
|
+
press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
|
|
334
|
+
or change the session. User and assistant messages, tool calls, and abbreviated
|
|
335
|
+
tool output are colour-coded, while internal context and encrypted reasoning
|
|
336
|
+
are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
|
|
337
|
+
projected onto the provider's current conversation: the retained prefix and
|
|
338
|
+
replacement suffix remain visible, while the abandoned suffix is hidden.
|
|
339
|
+
|
|
340
|
+
Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
341
|
+
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
342
|
+
per-pane history and normally preserves the live terminal's captured styling.
|
|
343
|
+
When Railmux confirms that a running Codex session advanced after rewind or
|
|
344
|
+
steering, it resets only the current terminal view before Codex redraws and
|
|
345
|
+
invalidates that pane's SSH history generation. Retained tmux scrollback is not
|
|
346
|
+
erased; the SSH history for that branched generation uses the canonical
|
|
347
|
+
rollout so its abandoned suffix stays hidden.
|
|
341
348
|
|
|
342
349
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
343
350
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.4.dev1"
|
|
@@ -403,8 +403,10 @@ class _PaneGeometry:
|
|
|
403
403
|
alternate_on: bool = False
|
|
404
404
|
transcript_source: str | None = None
|
|
405
405
|
transcript_backed: bool = False
|
|
406
|
+
transcript_provider: str | None = None
|
|
406
407
|
claude_history_policy: str = "ask"
|
|
407
408
|
history_generation: int = 0
|
|
409
|
+
canonical_history: bool = False
|
|
408
410
|
|
|
409
411
|
|
|
410
412
|
@dataclass(frozen=True)
|
|
@@ -431,11 +433,14 @@ _HISTORY_GENERATION_RE = re.compile(
|
|
|
431
433
|
|
|
432
434
|
def _history_generation(marker: str) -> int:
|
|
433
435
|
"""Map one validated provider UUID to a bounded opaque wire epoch."""
|
|
436
|
+
generation_marker = marker
|
|
437
|
+
if marker.startswith(tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX):
|
|
438
|
+
marker = marker[len(tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX):]
|
|
434
439
|
if not _HISTORY_GENERATION_RE.fullmatch(marker):
|
|
435
440
|
return 0
|
|
436
441
|
return int.from_bytes(
|
|
437
442
|
hashlib.blake2b(
|
|
438
|
-
|
|
443
|
+
generation_marker.lower().encode("ascii"),
|
|
439
444
|
digest_size=8,
|
|
440
445
|
person=b"railmux-history",
|
|
441
446
|
).digest(),
|
|
@@ -916,10 +921,11 @@ def _list_agent_panes(
|
|
|
916
921
|
if history_pane_id is None:
|
|
917
922
|
history_server = None
|
|
918
923
|
transcript_marker = fields[14] or None
|
|
919
|
-
|
|
920
|
-
transcript_marker
|
|
921
|
-
|
|
924
|
+
transcript_locator = (
|
|
925
|
+
tmux_server.decode_transcript_source(transcript_marker)
|
|
926
|
+
if transcript_marker is not None else None
|
|
922
927
|
)
|
|
928
|
+
transcript_backed = transcript_locator is not None
|
|
923
929
|
if not transcript_backed and fields[11] == "1" and fields[12] == "1":
|
|
924
930
|
transcript_backed, transcript_marker = _inferred_transcript_source(
|
|
925
931
|
session_id=session_id,
|
|
@@ -933,7 +939,17 @@ def _list_agent_panes(
|
|
|
933
939
|
opened = tmux_server.open_transcript_source(transcript_marker)
|
|
934
940
|
transcript_backed = opened is not None
|
|
935
941
|
if opened is not None:
|
|
942
|
+
transcript_locator = opened[0]
|
|
936
943
|
os.close(opened[1])
|
|
944
|
+
history_marker = fields[15]
|
|
945
|
+
canonical_history = bool(
|
|
946
|
+
transcript_backed
|
|
947
|
+
and transcript_locator is not None
|
|
948
|
+
and history_marker == (
|
|
949
|
+
f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}"
|
|
950
|
+
f"{transcript_locator.session_id}"
|
|
951
|
+
)
|
|
952
|
+
)
|
|
937
953
|
rows.append(
|
|
938
954
|
(
|
|
939
955
|
fields[2] == "1",
|
|
@@ -951,8 +967,14 @@ def _list_agent_panes(
|
|
|
951
967
|
alternate_on=fields[11] == "1",
|
|
952
968
|
transcript_source=transcript_marker,
|
|
953
969
|
transcript_backed=transcript_backed,
|
|
970
|
+
transcript_provider=(
|
|
971
|
+
transcript_locator.provider
|
|
972
|
+
if transcript_backed and transcript_locator is not None
|
|
973
|
+
else None
|
|
974
|
+
),
|
|
954
975
|
claude_history_policy=claude_history_policy,
|
|
955
|
-
history_generation=_history_generation(
|
|
976
|
+
history_generation=_history_generation(history_marker),
|
|
977
|
+
canonical_history=canonical_history,
|
|
956
978
|
),
|
|
957
979
|
)
|
|
958
980
|
)
|
|
@@ -1267,7 +1289,9 @@ def _transcript_rows(
|
|
|
1267
1289
|
return None
|
|
1268
1290
|
formatted = "".join(
|
|
1269
1291
|
transcript_renderer.format_transcript(
|
|
1270
|
-
io.StringIO(raw),
|
|
1292
|
+
io.StringIO(raw),
|
|
1293
|
+
source.provider,
|
|
1294
|
+
claude_native=source.provider == "claude",
|
|
1271
1295
|
)
|
|
1272
1296
|
)
|
|
1273
1297
|
rows, dropped = _wrap_transcript_rows(pyte, formatted, width)
|
|
@@ -1330,12 +1354,21 @@ def _capture_pane_history(
|
|
|
1330
1354
|
# pane's current viewport merely to retain older scrollback.
|
|
1331
1355
|
current_raw = raw_lines[-pane.height :]
|
|
1332
1356
|
transcript_entry = None
|
|
1357
|
+
# ``None`` is retained only for old/test geometry that predates the
|
|
1358
|
+
# provider field; every validated live marker carries an explicit value.
|
|
1359
|
+
transcript_provider = pane.transcript_provider or "claude"
|
|
1333
1360
|
if (
|
|
1334
1361
|
pane.transcript_backed
|
|
1335
|
-
and pane.claude_history_policy == "local"
|
|
1336
|
-
and pane.alternate_on
|
|
1337
|
-
and pane.history_size == 0
|
|
1338
1362
|
and pane.transcript_source
|
|
1363
|
+
and (
|
|
1364
|
+
(transcript_provider == "codex" and pane.canonical_history)
|
|
1365
|
+
or (
|
|
1366
|
+
transcript_provider == "claude"
|
|
1367
|
+
and pane.claude_history_policy == "local"
|
|
1368
|
+
and pane.alternate_on
|
|
1369
|
+
and pane.history_size == 0
|
|
1370
|
+
)
|
|
1371
|
+
)
|
|
1339
1372
|
):
|
|
1340
1373
|
transcript_entry = _transcript_rows(
|
|
1341
1374
|
pyte,
|
|
@@ -1387,7 +1420,9 @@ def _capture_pane_history(
|
|
|
1387
1420
|
transcript_backed=transcript_used,
|
|
1388
1421
|
transcript_available=pane.transcript_backed,
|
|
1389
1422
|
history_choice_required=(
|
|
1390
|
-
pane.transcript_backed
|
|
1423
|
+
pane.transcript_backed
|
|
1424
|
+
and transcript_provider == "claude"
|
|
1425
|
+
and pane.claude_history_policy == "ask"
|
|
1391
1426
|
),
|
|
1392
1427
|
more_available=(
|
|
1393
1428
|
budget_truncated
|
|
@@ -1434,6 +1434,18 @@ def set_pane_user_option(target_pane: str, name: str,
|
|
|
1434
1434
|
return False
|
|
1435
1435
|
|
|
1436
1436
|
|
|
1437
|
+
def pane_user_option(target_pane: str, name: str) -> str | None:
|
|
1438
|
+
"""Read one pane-local user option, returning ``None`` when unavailable."""
|
|
1439
|
+
try:
|
|
1440
|
+
value = subprocess.check_output(
|
|
1441
|
+
["tmux", "show-options", "-pv", "-t", target_pane, name],
|
|
1442
|
+
stderr=subprocess.DEVNULL,
|
|
1443
|
+
).decode().strip()
|
|
1444
|
+
except (OSError, subprocess.CalledProcessError, UnicodeError):
|
|
1445
|
+
return None
|
|
1446
|
+
return value or None
|
|
1447
|
+
|
|
1448
|
+
|
|
1437
1449
|
def unset_pane_user_option_if_value(target_pane: str, name: str,
|
|
1438
1450
|
expected: str) -> bool:
|
|
1439
1451
|
"""Unset a pane option only when Railmux still owns its exact value."""
|
|
@@ -1824,15 +1836,14 @@ _PREFIX_TARGET_KEY = "Tab"
|
|
|
1824
1836
|
_PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
1825
1837
|
RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
|
|
1826
1838
|
RAILMUX_TARGET_OPTION = "@railmux_target_pane"
|
|
1827
|
-
RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
|
|
1828
1839
|
RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
|
|
1829
|
-
#
|
|
1840
|
+
# A confirmed branch transition prefixes the opaque SSH history cache epoch.
|
|
1841
|
+
# Plain generations use native pane history; only this explicit marker permits
|
|
1842
|
+
# the canonical transcript fallback that removes an abandoned rewind suffix.
|
|
1843
|
+
RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical:"
|
|
1844
|
+
# tmux names function keys only through F12. Private restore routes use
|
|
1830
1845
|
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1831
1846
|
# user input forwarded to an agent pane.
|
|
1832
|
-
RAILMUX_HISTORY_PREVIEW_KEYS = {
|
|
1833
|
-
"primary": ("F21", "\x1b[35~"),
|
|
1834
|
-
"secondary": ("F22", "\x1b[36~"),
|
|
1835
|
-
}
|
|
1836
1847
|
RAILMUX_HISTORY_RESTORE_KEYS = {
|
|
1837
1848
|
"primary": ("F23", "\x1b[42~"),
|
|
1838
1849
|
"secondary": ("F24", "\x1b[43~"),
|
|
@@ -2165,69 +2176,30 @@ def set_root_wheel_forwarding(
|
|
|
2165
2176
|
backup: RootWheelBindingBackup,
|
|
2166
2177
|
token: str,
|
|
2167
2178
|
) -> bool:
|
|
2168
|
-
"""
|
|
2179
|
+
"""Preserve terminal-native wheel behavior under shared ownership.
|
|
2169
2180
|
|
|
2170
2181
|
The literal marker makes crash recovery able to distinguish Railmux's
|
|
2171
2182
|
wrapper from a binding the user installed while Railmux was running.
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
|
|
2176
|
-
unbound by default and therefore has no false branch.
|
|
2183
|
+
Mouse-aware panes receive the original report, while non-mouse-aware panes
|
|
2184
|
+
retain the exact stock fallback (including native copy-mode on wheel-up).
|
|
2185
|
+
A live wheel must never be converted into Railmux's transcript Preview.
|
|
2177
2186
|
"""
|
|
2178
2187
|
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2179
2188
|
mouse_condition = (
|
|
2180
2189
|
"#{&&:#{mouse_any_flag},"
|
|
2181
2190
|
f"#{{==:{marker},{marker}}}}}"
|
|
2182
2191
|
)
|
|
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
|
-
|
|
2199
2192
|
try:
|
|
2200
2193
|
for key in _ROOT_WHEEL_KEYS:
|
|
2201
2194
|
binding = backup.get(key)
|
|
2202
2195
|
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}'"
|
|
2208
2196
|
args = [
|
|
2209
2197
|
"tmux", "bind-key", "-T", "root", key,
|
|
2198
|
+
"if-shell", "-F", "-t", "=", mouse_condition,
|
|
2199
|
+
"send-keys -M",
|
|
2210
2200
|
]
|
|
2211
|
-
if
|
|
2212
|
-
|
|
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)
|
|
2201
|
+
if fallback:
|
|
2202
|
+
args.append(fallback)
|
|
2231
2203
|
subprocess.check_call(
|
|
2232
2204
|
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
2233
2205
|
except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
|
|
@@ -2248,7 +2220,7 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
|
|
|
2248
2220
|
|
|
2249
2221
|
|
|
2250
2222
|
def root_wheel_bindings_are_current(token: str) -> bool:
|
|
2251
|
-
"""Whether the owned wrapper
|
|
2223
|
+
"""Whether the owned wrapper preserves native live-pane scrolling."""
|
|
2252
2224
|
bindings = read_root_wheel_bindings()
|
|
2253
2225
|
up = bindings.get("WheelUpPane")
|
|
2254
2226
|
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
@@ -2261,11 +2233,14 @@ def root_wheel_bindings_are_current(token: str) -> bool:
|
|
|
2261
2233
|
for binding in bindings.values()
|
|
2262
2234
|
)
|
|
2263
2235
|
and up is not None
|
|
2264
|
-
and
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2236
|
+
and "copy-mode -e" in up
|
|
2237
|
+
# Reject the 0.3.3 wrapper during a soft upgrade. It routed live wheel
|
|
2238
|
+
# input through private F21/F22 keys into transcript Preview.
|
|
2239
|
+
and "@railmux_history_preview_v1" not in up
|
|
2240
|
+
and r"\033[35~" not in up
|
|
2241
|
+
and r"\033[36~" not in up
|
|
2242
|
+
and "\x1b[35~" not in up
|
|
2243
|
+
and "\x1b[36~" not in up
|
|
2269
2244
|
)
|
|
2270
2245
|
|
|
2271
2246
|
|
|
@@ -3355,14 +3330,15 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
|
|
|
3355
3330
|
)
|
|
3356
3331
|
|
|
3357
3332
|
|
|
3358
|
-
def
|
|
3359
|
-
"""Reset visible
|
|
3333
|
+
def reset_pane_view(identity: PaneIdentity) -> bool:
|
|
3334
|
+
"""Reset visible terminal state without deleting retained scrollback.
|
|
3360
3335
|
|
|
3361
3336
|
Revalidate the immutable session, pane process, and window before issuing
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3337
|
+
tmux's terminal reset. Then send the pane's foreground process group an
|
|
3338
|
+
ordinary same-size ``SIGWINCH`` so its TUI repaints and reasserts terminal
|
|
3339
|
+
modes without a geometry change or injected keyboard input. The reset can
|
|
3340
|
+
push the old viewport into tmux history, but never destroys the retained
|
|
3341
|
+
prefix; managed history surfaces use the provider's canonical transcript.
|
|
3366
3342
|
"""
|
|
3367
3343
|
current = pane_identity(identity.pane_id)
|
|
3368
3344
|
if (current is None or current.dead
|
|
@@ -3372,10 +3348,7 @@ def reset_pane_history(identity: PaneIdentity) -> bool:
|
|
|
3372
3348
|
return False
|
|
3373
3349
|
try:
|
|
3374
3350
|
subprocess.check_call(
|
|
3375
|
-
[
|
|
3376
|
-
"tmux", "send-keys", "-R", "-t", identity.pane_id,
|
|
3377
|
-
";", "clear-history", "-t", identity.pane_id,
|
|
3378
|
-
],
|
|
3351
|
+
["tmux", "send-keys", "-R", "-t", identity.pane_id],
|
|
3379
3352
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
3380
3353
|
)
|
|
3381
3354
|
except (OSError, subprocess.CalledProcessError):
|
|
@@ -307,11 +307,21 @@ def encode_transcript_source(
|
|
|
307
307
|
) -> str | None:
|
|
308
308
|
"""Encode an exact transcript hint for a displayed managed agent pane."""
|
|
309
309
|
raw_path = str(path)
|
|
310
|
+
valid_name = (
|
|
311
|
+
path.name == f"{session_id}.jsonl"
|
|
312
|
+
if provider == "claude"
|
|
313
|
+
else (
|
|
314
|
+
path.name.startswith("rollout-")
|
|
315
|
+
and path.name.endswith(f"-{session_id}.jsonl")
|
|
316
|
+
)
|
|
317
|
+
if provider == "codex"
|
|
318
|
+
else False
|
|
319
|
+
)
|
|
310
320
|
if (
|
|
311
|
-
provider
|
|
321
|
+
provider not in {"claude", "codex"}
|
|
312
322
|
or not _SESSION_ID_RE.fullmatch(session_id)
|
|
313
323
|
or not path.is_absolute()
|
|
314
|
-
or
|
|
324
|
+
or not valid_name
|
|
315
325
|
or len(raw_path) > 4096
|
|
316
326
|
):
|
|
317
327
|
return None
|
|
@@ -348,7 +358,7 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
|
|
|
348
358
|
raw_path = payload.get("path")
|
|
349
359
|
if (
|
|
350
360
|
payload.get("schema_version") != _TRANSCRIPT_SOURCE_SCHEMA
|
|
351
|
-
or provider
|
|
361
|
+
or provider not in {"claude", "codex"}
|
|
352
362
|
or not isinstance(session_id, str)
|
|
353
363
|
or not _SESSION_ID_RE.fullmatch(session_id)
|
|
354
364
|
or not isinstance(raw_path, str)
|
|
@@ -357,7 +367,15 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
|
|
|
357
367
|
):
|
|
358
368
|
return None
|
|
359
369
|
path = Path(raw_path)
|
|
360
|
-
|
|
370
|
+
valid_name = (
|
|
371
|
+
path.name == f"{session_id}.jsonl"
|
|
372
|
+
if provider == "claude"
|
|
373
|
+
else (
|
|
374
|
+
path.name.startswith("rollout-")
|
|
375
|
+
and path.name.endswith(f"-{session_id}.jsonl")
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
if not path.is_absolute() or not valid_name:
|
|
361
379
|
return None
|
|
362
380
|
return TranscriptSource(provider, session_id, path)
|
|
363
381
|
|