railmux 0.3.3.dev6__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.dev6/src/railmux.egg-info → railmux-0.3.4.dev1}/PKG-INFO +17 -12
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/README.md +16 -11
- railmux-0.3.4.dev1/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_server.py +17 -3
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_ctl.py +34 -59
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/app.py +36 -57
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1/src/railmux.egg-info}/PKG-INFO +17 -12
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_lineage.py +49 -1
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_fast_full_window.py +85 -1
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_focus_highlight.py +4 -14
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_ctl.py +24 -10
- railmux-0.3.3.dev6/src/railmux/__init__.py +0 -1
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/LICENSE +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/pyproject.toml +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/setup.cfg +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/transcript.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_display_transport.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_statusbar.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_transcript.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tools/release_notes.py +0 -0
- {railmux-0.3.3.dev6 → 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
|
|
|
@@ -372,16 +372,21 @@ sessions move ahead of the other results.
|
|
|
372
372
|
|
|
373
373
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
374
374
|
the Target pane without starting or resuming the agent. For a running session,
|
|
375
|
-
press
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
history
|
|
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.
|
|
385
390
|
|
|
386
391
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
387
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
|
|
|
@@ -330,16 +330,21 @@ sessions move ahead of the other results.
|
|
|
330
330
|
|
|
331
331
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
332
332
|
the Target pane without starting or resuming the agent. For a running session,
|
|
333
|
-
press
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
history
|
|
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.
|
|
343
348
|
|
|
344
349
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
345
350
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.4.dev1"
|
|
@@ -406,6 +406,7 @@ class _PaneGeometry:
|
|
|
406
406
|
transcript_provider: str | None = None
|
|
407
407
|
claude_history_policy: str = "ask"
|
|
408
408
|
history_generation: int = 0
|
|
409
|
+
canonical_history: bool = False
|
|
409
410
|
|
|
410
411
|
|
|
411
412
|
@dataclass(frozen=True)
|
|
@@ -432,11 +433,14 @@ _HISTORY_GENERATION_RE = re.compile(
|
|
|
432
433
|
|
|
433
434
|
def _history_generation(marker: str) -> int:
|
|
434
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):]
|
|
435
439
|
if not _HISTORY_GENERATION_RE.fullmatch(marker):
|
|
436
440
|
return 0
|
|
437
441
|
return int.from_bytes(
|
|
438
442
|
hashlib.blake2b(
|
|
439
|
-
|
|
443
|
+
generation_marker.lower().encode("ascii"),
|
|
440
444
|
digest_size=8,
|
|
441
445
|
person=b"railmux-history",
|
|
442
446
|
).digest(),
|
|
@@ -937,6 +941,15 @@ def _list_agent_panes(
|
|
|
937
941
|
if opened is not None:
|
|
938
942
|
transcript_locator = opened[0]
|
|
939
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
|
+
)
|
|
940
953
|
rows.append(
|
|
941
954
|
(
|
|
942
955
|
fields[2] == "1",
|
|
@@ -960,7 +973,8 @@ def _list_agent_panes(
|
|
|
960
973
|
else None
|
|
961
974
|
),
|
|
962
975
|
claude_history_policy=claude_history_policy,
|
|
963
|
-
history_generation=_history_generation(
|
|
976
|
+
history_generation=_history_generation(history_marker),
|
|
977
|
+
canonical_history=canonical_history,
|
|
964
978
|
),
|
|
965
979
|
)
|
|
966
980
|
)
|
|
@@ -1347,7 +1361,7 @@ def _capture_pane_history(
|
|
|
1347
1361
|
pane.transcript_backed
|
|
1348
1362
|
and pane.transcript_source
|
|
1349
1363
|
and (
|
|
1350
|
-
transcript_provider == "codex"
|
|
1364
|
+
(transcript_provider == "codex" and pane.canonical_history)
|
|
1351
1365
|
or (
|
|
1352
1366
|
transcript_provider == "claude"
|
|
1353
1367
|
and pane.claude_history_policy == "local"
|
|
@@ -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
|
|
|
@@ -1117,7 +1117,7 @@ class App:
|
|
|
1117
1117
|
session_id: str | None,
|
|
1118
1118
|
tmux_name: str | None,
|
|
1119
1119
|
) -> None:
|
|
1120
|
-
"""Stamp transcript
|
|
1120
|
+
"""Stamp the explicit transcript Preview source for a live agent."""
|
|
1121
1121
|
pane_id = slot.pane_id
|
|
1122
1122
|
if pane_id is None or not pane_id.startswith("%"):
|
|
1123
1123
|
return
|
|
@@ -1156,19 +1156,6 @@ class App:
|
|
|
1156
1156
|
tmux_server.TRANSCRIPT_SOURCE_OPTION,
|
|
1157
1157
|
marker,
|
|
1158
1158
|
)
|
|
1159
|
-
history_slot = None
|
|
1160
|
-
if (
|
|
1161
|
-
running is not None
|
|
1162
|
-
and running.session_type in ("claude", "codex")
|
|
1163
|
-
and logical_id is not None
|
|
1164
|
-
and not running.is_placeholder
|
|
1165
|
-
):
|
|
1166
|
-
history_slot = slot.key
|
|
1167
|
-
tmux_ctl.set_pane_user_option(
|
|
1168
|
-
pane_id,
|
|
1169
|
-
tmux_ctl.RAILMUX_HISTORY_PREVIEW_OPTION,
|
|
1170
|
-
history_slot,
|
|
1171
|
-
)
|
|
1172
1159
|
|
|
1173
1160
|
def _paint_slot_active_target(
|
|
1174
1161
|
self,
|
|
@@ -1973,9 +1960,9 @@ class App:
|
|
|
1973
1960
|
def _on_session_preview(self, session: SessionMeta) -> None:
|
|
1974
1961
|
"""Show the provider's canonical current-branch history.
|
|
1975
1962
|
|
|
1976
|
-
Stopped-row clicks, Space, context Preview
|
|
1977
|
-
|
|
1978
|
-
|
|
1963
|
+
Stopped-row clicks, Space, and context Preview use this read-only
|
|
1964
|
+
projection. A Codex rewind lineage is represented by its newest
|
|
1965
|
+
rollout; Claude branches are projected by ``transcript``.
|
|
1979
1966
|
"""
|
|
1980
1967
|
if session.session_type == "codex":
|
|
1981
1968
|
representative = self._codex_representative(session.session_id)
|
|
@@ -2038,20 +2025,6 @@ class App:
|
|
|
2038
2025
|
self._set_workspace_target(slot.key)
|
|
2039
2026
|
self._on_session_preview(session)
|
|
2040
2027
|
|
|
2041
|
-
def _preview_slot_history(self, slot: AgentSlot) -> None:
|
|
2042
|
-
"""Handle a pane-local wheel-up routed through the controller."""
|
|
2043
|
-
tmux_name = slot.agent_tmux_name
|
|
2044
|
-
if tmux_name is None:
|
|
2045
|
-
return
|
|
2046
|
-
running = self._by_tmux(tmux_name)
|
|
2047
|
-
if running is None:
|
|
2048
|
-
return
|
|
2049
|
-
self._preview_running_history(RunningEntry(
|
|
2050
|
-
tmux_name=tmux_name,
|
|
2051
|
-
label=running.label,
|
|
2052
|
-
status=running.status,
|
|
2053
|
-
))
|
|
2054
|
-
|
|
2055
2028
|
def _restore_history_slot(self, slot: AgentSlot) -> None:
|
|
2056
2029
|
"""Return a closed live-history viewer to the exact saved agent."""
|
|
2057
2030
|
if not slot.in_history_mode:
|
|
@@ -7458,18 +7431,6 @@ class App:
|
|
|
7458
7431
|
if key == COMPACT_RESIZE_KEY.lower():
|
|
7459
7432
|
self._handle_remote_compact_prepare()
|
|
7460
7433
|
return
|
|
7461
|
-
for slot_key, (preview_key, _sequence) in (
|
|
7462
|
-
tmux_ctl.RAILMUX_HISTORY_PREVIEW_KEYS.items()):
|
|
7463
|
-
if key == preview_key.lower():
|
|
7464
|
-
if (getattr(self, "_loop", None) is not None
|
|
7465
|
-
and self._loop.widget is not self._frame):
|
|
7466
|
-
return
|
|
7467
|
-
workspace = self._agent_workspace()
|
|
7468
|
-
self._preview_slot_history(
|
|
7469
|
-
workspace.primary
|
|
7470
|
-
if slot_key == AgentWorkspace.PRIMARY
|
|
7471
|
-
else workspace.secondary)
|
|
7472
|
-
return
|
|
7473
7434
|
for slot_key, (restore_key, _sequence) in (
|
|
7474
7435
|
tmux_ctl.RAILMUX_HISTORY_RESTORE_KEYS.items()):
|
|
7475
7436
|
if key == restore_key.lower():
|
|
@@ -8627,11 +8588,13 @@ class App:
|
|
|
8627
8588
|
therefore survive in raw tmux scrollback even though the canonical
|
|
8628
8589
|
JSONL is already correct. Baseline the first observed rollout, then
|
|
8629
8590
|
reset only the live terminal view when the next canonical rollout names
|
|
8630
|
-
that baseline as its parent.
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8591
|
+
that baseline as its parent. Local wheel scrolling remains terminal-
|
|
8592
|
+
native. The SSH display keeps owning wheel history and normally reads
|
|
8593
|
+
raw pane capture; only this confirmed transition marks that history as
|
|
8594
|
+
canonical-transcript-backed so the abandoned suffix stays hidden. On
|
|
8595
|
+
procfs systems the new rollout must also be open in this exact pane's
|
|
8596
|
+
process tree; an unavailable probe degrades to the provider lineage
|
|
8597
|
+
link, while a negative probe waits for a later refresh.
|
|
8635
8598
|
"""
|
|
8636
8599
|
current_id = meta.session_id
|
|
8637
8600
|
previous_id = running.codex_canonical_session_id
|
|
@@ -8663,7 +8626,7 @@ class App:
|
|
|
8663
8626
|
if identity is None or not tmux_ctl.reset_pane_view(identity):
|
|
8664
8627
|
return
|
|
8665
8628
|
running.codex_canonical_session_id = current_id
|
|
8666
|
-
self._stamp_codex_history_state(running, meta)
|
|
8629
|
+
self._stamp_codex_history_state(running, meta, canonical=True)
|
|
8667
8630
|
|
|
8668
8631
|
def _codex_real_pane_identity(
|
|
8669
8632
|
self, running: _Running,
|
|
@@ -8677,9 +8640,18 @@ class App:
|
|
|
8677
8640
|
return topology.single_live_pane if topology is not None else None
|
|
8678
8641
|
|
|
8679
8642
|
def _stamp_codex_history_state(
|
|
8680
|
-
self,
|
|
8643
|
+
self,
|
|
8644
|
+
running: _Running,
|
|
8645
|
+
meta: SessionMeta,
|
|
8646
|
+
*,
|
|
8647
|
+
canonical: bool = False,
|
|
8681
8648
|
) -> None:
|
|
8682
|
-
"""Publish
|
|
8649
|
+
"""Publish Preview source plus the SSH history-source cache epoch.
|
|
8650
|
+
|
|
8651
|
+
A plain UUID keeps managed SSH history on raw pane capture. Only a
|
|
8652
|
+
confirmed rewind transition writes ``canonical:<uuid>`` and permits a
|
|
8653
|
+
transcript projection. Preserve that exact marker across app restarts.
|
|
8654
|
+
"""
|
|
8683
8655
|
if running.is_legacy:
|
|
8684
8656
|
return
|
|
8685
8657
|
identity = self._codex_real_pane_identity(running)
|
|
@@ -8704,14 +8676,21 @@ class App:
|
|
|
8704
8676
|
) for pane_id in sorted(pane_ids)]
|
|
8705
8677
|
if marker is not None else []
|
|
8706
8678
|
)
|
|
8707
|
-
|
|
8708
|
-
tmux_ctl.
|
|
8679
|
+
canonical_generation = (
|
|
8680
|
+
f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}{meta.session_id}"
|
|
8681
|
+
)
|
|
8682
|
+
generation_results = []
|
|
8683
|
+
for pane_id in sorted(pane_ids):
|
|
8684
|
+
existing = tmux_ctl.pane_user_option(
|
|
8685
|
+
pane_id, tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION)
|
|
8686
|
+
if not canonical and existing == canonical_generation:
|
|
8687
|
+
generation_results.append(True)
|
|
8688
|
+
continue
|
|
8689
|
+
generation_results.append(tmux_ctl.set_pane_user_option(
|
|
8709
8690
|
pane_id,
|
|
8710
8691
|
tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
|
|
8711
|
-
meta.session_id,
|
|
8712
|
-
)
|
|
8713
|
-
for pane_id in sorted(pane_ids)
|
|
8714
|
-
]
|
|
8692
|
+
canonical_generation if canonical else meta.session_id,
|
|
8693
|
+
))
|
|
8715
8694
|
transcript_stamped = marker is not None and all(transcript_results)
|
|
8716
8695
|
generation_stamped = all(generation_results)
|
|
8717
8696
|
running.codex_history_generation_stamped = bool(
|
|
@@ -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
|
|
|
@@ -372,16 +372,21 @@ sessions move ahead of the other results.
|
|
|
372
372
|
|
|
373
373
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
374
374
|
the Target pane without starting or resuming the agent. For a running session,
|
|
375
|
-
press
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
history
|
|
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.
|
|
385
390
|
|
|
386
391
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
387
392
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -106,6 +106,7 @@ def test_live_rewind_resets_view_and_stamps_canonical_transcript(
|
|
|
106
106
|
monkeypatch.setattr(tmux_ctl, "reset_pane_view", reset)
|
|
107
107
|
stamped = MagicMock(return_value=True)
|
|
108
108
|
monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
|
|
109
|
+
monkeypatch.setattr(tmux_ctl, "pane_user_option", lambda *_args: None)
|
|
109
110
|
probes: dict[str, set[str] | None] = {}
|
|
110
111
|
|
|
111
112
|
# An indexed child is not enough: on procfs the exact live writer must
|
|
@@ -127,7 +128,10 @@ def test_live_rewind_resets_view_and_stamps_canonical_transcript(
|
|
|
127
128
|
)
|
|
128
129
|
assert leaf_id in transcript_call.args[2]
|
|
129
130
|
assert generation_call.args == (
|
|
130
|
-
"%9",
|
|
131
|
+
"%9",
|
|
132
|
+
tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
|
|
133
|
+
f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}{leaf_id}",
|
|
134
|
+
)
|
|
131
135
|
|
|
132
136
|
|
|
133
137
|
def test_first_codex_generation_only_establishes_scrollback_baseline(
|
|
@@ -197,9 +201,53 @@ def test_codex_history_state_stamps_nested_wrapper_and_real_pane(
|
|
|
197
201
|
app._codex_real_pane_identity = MagicMock(return_value=identity)
|
|
198
202
|
stamped = MagicMock(return_value=True)
|
|
199
203
|
monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
|
|
204
|
+
monkeypatch.setattr(tmux_ctl, "pane_user_option", lambda *_args: None)
|
|
200
205
|
|
|
201
206
|
app._stamp_codex_history_state(running, meta)
|
|
202
207
|
|
|
203
208
|
assert running.codex_history_generation_stamped
|
|
204
209
|
assert {call.args[0] for call in stamped.call_args_list} == {"%9", "%10"}
|
|
205
210
|
assert stamped.call_count == 4
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_codex_history_state_preserves_confirmed_canonical_marker_on_restart(
|
|
214
|
+
monkeypatch, tmp_path,
|
|
215
|
+
) -> None:
|
|
216
|
+
session_id = "019fc7c1-a27c-7ae0-9937-7570552a112a"
|
|
217
|
+
project = Project(tmp_path, "-project", Path(), 1, 0.0)
|
|
218
|
+
running = _Running(
|
|
219
|
+
key=session_id,
|
|
220
|
+
tmux_name="cx-live",
|
|
221
|
+
label="project/conversation",
|
|
222
|
+
project=project,
|
|
223
|
+
session_type="codex",
|
|
224
|
+
)
|
|
225
|
+
meta = SessionMeta(
|
|
226
|
+
project=project,
|
|
227
|
+
session_id=session_id,
|
|
228
|
+
jsonl_path=tmp_path / (
|
|
229
|
+
f"rollout-2026-08-03T13-13-03-{session_id}.jsonl"),
|
|
230
|
+
title="current",
|
|
231
|
+
message_count=2,
|
|
232
|
+
token_total=0,
|
|
233
|
+
last_mtime=2.0,
|
|
234
|
+
session_type="codex",
|
|
235
|
+
)
|
|
236
|
+
identity = tmux_ctl.PaneIdentity(
|
|
237
|
+
"%9", 123, "cx-live", "$4", "@5", False, 80, 24)
|
|
238
|
+
app = App.__new__(App)
|
|
239
|
+
app._codex_real_pane_identity = MagicMock(return_value=identity)
|
|
240
|
+
stamped = MagicMock(return_value=True)
|
|
241
|
+
monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
|
|
242
|
+
canonical_marker = (
|
|
243
|
+
f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}{session_id}"
|
|
244
|
+
)
|
|
245
|
+
monkeypatch.setattr(
|
|
246
|
+
tmux_ctl, "pane_user_option", lambda *_args: canonical_marker)
|
|
247
|
+
|
|
248
|
+
app._stamp_codex_history_state(running, meta)
|
|
249
|
+
|
|
250
|
+
assert running.codex_history_generation_stamped
|
|
251
|
+
assert stamped.call_count == 1
|
|
252
|
+
assert stamped.call_args.args[:2] == (
|
|
253
|
+
"%9", tmux_server.TRANSCRIPT_SOURCE_OPTION)
|