railmux 0.3.3.dev3__tar.gz → 0.3.3.dev5__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.dev3/src/railmux.egg-info → railmux-0.3.3.dev5}/PKG-INFO +9 -3
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/README.md +8 -2
- railmux-0.3.3.dev5/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/fast_display_client.py +2 -2
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/fast_display_history.py +12 -3
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/fast_display_protocol.py +20 -7
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/fast_display_server.py +26 -3
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/tmux_ctl.py +49 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/app.py +364 -123
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5/src/railmux.egg-info}/PKG-INFO +9 -3
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_codex_delete.py +134 -39
- railmux-0.3.3.dev5/tests/test_codex_lineage.py +157 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_fast_full_window.py +125 -12
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_release_workflow.py +5 -2
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_statusbar.py +16 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tmux_ctl.py +37 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tmux_integration.py +41 -0
- railmux-0.3.3.dev3/src/railmux/__init__.py +0 -1
- railmux-0.3.3.dev3/tests/test_codex_lineage.py +0 -64
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/LICENSE +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/pyproject.toml +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/setup.cfg +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/transcript.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_display_transport.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_transcript.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/tools/release_notes.py +0 -0
- {railmux-0.3.3.dev3 → railmux-0.3.3.dev5}/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.dev5
|
|
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
|
|
@@ -204,7 +204,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
204
204
|
| `r` | Rename the focused session |
|
|
205
205
|
| `s` | Toggle star — starred sessions pinned to top with ⭐ |
|
|
206
206
|
| `k` | Kill the running agent process (keeps session file) |
|
|
207
|
-
| `d` | Delete the focused session (
|
|
207
|
+
| `d` | Delete the focused session (confirms, then finishes in background) |
|
|
208
208
|
| `t` | Open or return to the managed terminal below the Target agent |
|
|
209
209
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
210
210
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
@@ -242,6 +242,10 @@ The rename popup starts with the current title pre-filled. Press
|
|
|
242
242
|
`Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
|
|
243
243
|
to cancel.
|
|
244
244
|
|
|
245
|
+
After a delete is confirmed, its popup closes immediately. The bottom-right
|
|
246
|
+
status shows **Deleting…** while Railmux stops the exact provider session and
|
|
247
|
+
removes its history in the background, then reports the final result.
|
|
248
|
+
|
|
245
249
|
Press `/` in Projects, Sessions, or Running to filter that section. Active
|
|
246
250
|
filters are marked with `[filtered]` and survive a soft restart; reopening `/`
|
|
247
251
|
shows the current value. Press `Ctrl-U` in the filter editor to clear it, then
|
|
@@ -373,7 +377,9 @@ messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
|
373
377
|
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
374
378
|
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
375
379
|
conversation: the retained prefix and replacement suffix remain visible, while
|
|
376
|
-
the abandoned suffix is hidden.
|
|
380
|
+
the abandoned suffix is hidden. When Railmux observes a running Codex session
|
|
381
|
+
advance to its rewind child, it also removes the abandoned terminal screen and
|
|
382
|
+
tmux scrollback before Codex redraws the live pane.
|
|
377
383
|
|
|
378
384
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
379
385
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -162,7 +162,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
162
162
|
| `r` | Rename the focused session |
|
|
163
163
|
| `s` | Toggle star — starred sessions pinned to top with ⭐ |
|
|
164
164
|
| `k` | Kill the running agent process (keeps session file) |
|
|
165
|
-
| `d` | Delete the focused session (
|
|
165
|
+
| `d` | Delete the focused session (confirms, then finishes in background) |
|
|
166
166
|
| `t` | Open or return to the managed terminal below the Target agent |
|
|
167
167
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
168
168
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
@@ -200,6 +200,10 @@ The rename popup starts with the current title pre-filled. Press
|
|
|
200
200
|
`Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
|
|
201
201
|
to cancel.
|
|
202
202
|
|
|
203
|
+
After a delete is confirmed, its popup closes immediately. The bottom-right
|
|
204
|
+
status shows **Deleting…** while Railmux stops the exact provider session and
|
|
205
|
+
removes its history in the background, then reports the final result.
|
|
206
|
+
|
|
203
207
|
Press `/` in Projects, Sessions, or Running to filter that section. Active
|
|
204
208
|
filters are marked with `[filtered]` and survive a soft restart; reopening `/`
|
|
205
209
|
shows the current value. Press `Ctrl-U` in the filter editor to clear it, then
|
|
@@ -331,7 +335,9 @@ messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
|
331
335
|
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
332
336
|
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
333
337
|
conversation: the retained prefix and replacement suffix remain visible, while
|
|
334
|
-
the abandoned suffix is hidden.
|
|
338
|
+
the abandoned suffix is hidden. When Railmux observes a running Codex session
|
|
339
|
+
advance to its rewind child, it also removes the abandoned terminal screen and
|
|
340
|
+
tmux scrollback before Codex redraws the live pane.
|
|
335
341
|
|
|
336
342
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
337
343
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.3.dev5"
|
|
@@ -1074,7 +1074,7 @@ class TerminalSurface:
|
|
|
1074
1074
|
self,
|
|
1075
1075
|
requested: TerminalMode,
|
|
1076
1076
|
) -> TerminalMode:
|
|
1077
|
-
"""Mirror only input-affecting modes explicitly carried by protocol
|
|
1077
|
+
"""Mirror only input-affecting modes explicitly carried by protocol v15."""
|
|
1078
1078
|
disabled = self.terminal_modes & ~requested
|
|
1079
1079
|
enabled = requested & ~self.terminal_modes
|
|
1080
1080
|
controls: list[bytes] = []
|
|
@@ -2013,7 +2013,7 @@ def _finish_remote_attach(
|
|
|
2013
2013
|
raise ProbeError("remote display helper failed before attaching")
|
|
2014
2014
|
|
|
2015
2015
|
_stop_unstarted_remote(process)
|
|
2016
|
-
# A current
|
|
2016
|
+
# A current v15 helper holds the mutex only while registering its exact tmux
|
|
2017
2017
|
# child. Give that ordinary race one fresh SSH process before presenting
|
|
2018
2018
|
# the explicit legacy-lock takeover choice.
|
|
2019
2019
|
time.sleep(_REMOTE_ATTACH_RETRY_DELAY)
|
|
@@ -207,7 +207,11 @@ class LocalHistoryView:
|
|
|
207
207
|
restore_live = False
|
|
208
208
|
for pane_id, viewport in tuple(self.viewports.items()):
|
|
209
209
|
route = routes.get(pane_id)
|
|
210
|
-
if
|
|
210
|
+
if (
|
|
211
|
+
route is None
|
|
212
|
+
or not self._same_geometry(viewport.snapshot, route)
|
|
213
|
+
or not self._history_source_matches(viewport.snapshot, route)
|
|
214
|
+
):
|
|
211
215
|
self.cancel_pane(pane_id)
|
|
212
216
|
restore_live = True
|
|
213
217
|
return HistoryAction(restore_live=restore_live)
|
|
@@ -217,10 +221,11 @@ class LocalHistoryView:
|
|
|
217
221
|
left: HistorySnapshot,
|
|
218
222
|
right: HistorySnapshot,
|
|
219
223
|
) -> bool:
|
|
220
|
-
"""Do not combine
|
|
224
|
+
"""Do not combine different providers or terminal generations."""
|
|
221
225
|
return (
|
|
222
226
|
left.transcript_backed == right.transcript_backed
|
|
223
227
|
and left.history_choice_required == right.history_choice_required
|
|
228
|
+
and left.generation == right.generation
|
|
224
229
|
)
|
|
225
230
|
|
|
226
231
|
@staticmethod
|
|
@@ -813,7 +818,11 @@ class LocalHistoryView:
|
|
|
813
818
|
),
|
|
814
819
|
None,
|
|
815
820
|
)
|
|
816
|
-
if
|
|
821
|
+
if (
|
|
822
|
+
route is None
|
|
823
|
+
or not self._same_geometry(route, snapshot)
|
|
824
|
+
or not self._history_source_matches(route, snapshot)
|
|
825
|
+
):
|
|
817
826
|
return HistoryAction()
|
|
818
827
|
viewport = self.viewports.get(pending.pane_id)
|
|
819
828
|
if viewport is None:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Private
|
|
1
|
+
"""Private v15 framing for the coalesced full-window SSH display."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,9 +8,9 @@ from dataclasses import dataclass
|
|
|
8
8
|
from enum import IntEnum, IntFlag
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
DISPLAY_MAGIC = b"
|
|
12
|
-
INPUT_MAGIC = b"
|
|
13
|
-
PROTOCOL_VERSION =
|
|
11
|
+
DISPLAY_MAGIC = b"RMUXD15\x00"
|
|
12
|
+
INPUT_MAGIC = b"RMUXK15\x00"
|
|
13
|
+
PROTOCOL_VERSION = 15
|
|
14
14
|
REMOTE_CONFIG_PROTOCOL = 1
|
|
15
15
|
LENGTH_BYTES = 4
|
|
16
16
|
REMOTE_HELLO_PREFIX = b"RAILMUX-REMOTE/1 "
|
|
@@ -28,10 +28,10 @@ MAX_HISTORY_PANES = 8
|
|
|
28
28
|
MAX_CLIPBOARD_BYTES = 64 * 1024
|
|
29
29
|
MAX_PATH_BYTES = 4096
|
|
30
30
|
_UPDATE_METADATA = struct.Struct(">BIHHHHBHI")
|
|
31
|
-
_HISTORY_METADATA = struct.Struct(">
|
|
31
|
+
_HISTORY_METADATA = struct.Struct(">IIHHHHBQI")
|
|
32
32
|
_HISTORY_REQUEST = struct.Struct(">IHHH")
|
|
33
33
|
_HISTORY_BATCH_METADATA = struct.Struct(">II")
|
|
34
|
-
_HISTORY_PANE_METADATA = struct.Struct(">
|
|
34
|
+
_HISTORY_PANE_METADATA = struct.Struct(">IHHHHBQ")
|
|
35
35
|
_PREFETCH_HISTORY_REQUEST = struct.Struct(">IH")
|
|
36
36
|
_PATH_REQUEST = struct.Struct(">IIH")
|
|
37
37
|
_PATH_RESULT = struct.Struct(">IBBH")
|
|
@@ -143,6 +143,7 @@ class HistorySnapshot:
|
|
|
143
143
|
more_available: bool = False
|
|
144
144
|
transcript_available: bool = False
|
|
145
145
|
history_choice_required: bool = False
|
|
146
|
+
generation: int = 0
|
|
146
147
|
|
|
147
148
|
|
|
148
149
|
@dataclass(frozen=True)
|
|
@@ -302,6 +303,7 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
|
|
|
302
303
|
snapshot.more_available,
|
|
303
304
|
snapshot.transcript_available,
|
|
304
305
|
snapshot.history_choice_required,
|
|
306
|
+
snapshot.generation,
|
|
305
307
|
)):
|
|
306
308
|
raise ValueError("a rejected history response must be empty")
|
|
307
309
|
pane_number = 0
|
|
@@ -317,6 +319,8 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
|
|
|
317
319
|
raise ValueError("invalid history position")
|
|
318
320
|
if len(snapshot.lines) < snapshot.height:
|
|
319
321
|
raise ValueError("history snapshot is shorter than its viewport")
|
|
322
|
+
if not 0 <= snapshot.generation <= 0xFFFFFFFFFFFFFFFF:
|
|
323
|
+
raise ValueError("invalid history generation")
|
|
320
324
|
raw_lines = _pack_history_lines(snapshot.lines)
|
|
321
325
|
compressed = zlib.compress(raw_lines, level=3)
|
|
322
326
|
metadata = _HISTORY_METADATA.pack(
|
|
@@ -327,6 +331,7 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
|
|
|
327
331
|
snapshot.width,
|
|
328
332
|
snapshot.height,
|
|
329
333
|
_history_capabilities(snapshot),
|
|
334
|
+
snapshot.generation,
|
|
330
335
|
len(raw_lines),
|
|
331
336
|
)
|
|
332
337
|
payload = bytes((int(OutputKind.HISTORY),)) + metadata + compressed
|
|
@@ -360,6 +365,8 @@ def encode_history_batch(batch: HistoryBatch) -> bytes:
|
|
|
360
365
|
raise ValueError("invalid history position")
|
|
361
366
|
if len(snapshot.lines) < snapshot.height:
|
|
362
367
|
raise ValueError("history snapshot is shorter than its viewport")
|
|
368
|
+
if not 0 <= snapshot.generation <= 0xFFFFFFFFFFFFFFFF:
|
|
369
|
+
raise ValueError("invalid history generation")
|
|
363
370
|
packed_lines = _pack_history_lines(snapshot.lines)
|
|
364
371
|
pane_metadata = _HISTORY_PANE_METADATA.pack(
|
|
365
372
|
pane_number,
|
|
@@ -368,6 +375,7 @@ def encode_history_batch(batch: HistoryBatch) -> bytes:
|
|
|
368
375
|
snapshot.width,
|
|
369
376
|
snapshot.height,
|
|
370
377
|
_history_capabilities(snapshot),
|
|
378
|
+
snapshot.generation,
|
|
371
379
|
)
|
|
372
380
|
total += len(pane_metadata) + len(packed_lines)
|
|
373
381
|
if total > MAX_SCREEN_BYTES:
|
|
@@ -611,6 +619,7 @@ class ServerMessageDecoder:
|
|
|
611
619
|
width,
|
|
612
620
|
height,
|
|
613
621
|
raw_capabilities,
|
|
622
|
+
generation,
|
|
614
623
|
raw_size,
|
|
615
624
|
) = (
|
|
616
625
|
_HISTORY_METADATA.unpack(body[:_HISTORY_METADATA.size])
|
|
@@ -628,6 +637,7 @@ class ServerMessageDecoder:
|
|
|
628
637
|
height,
|
|
629
638
|
lines,
|
|
630
639
|
raw_capabilities,
|
|
640
|
+
generation,
|
|
631
641
|
)):
|
|
632
642
|
raise ValueError("invalid rejected history response")
|
|
633
643
|
pane_id = None
|
|
@@ -662,6 +672,7 @@ class ServerMessageDecoder:
|
|
|
662
672
|
history_choice_required=bool(
|
|
663
673
|
raw_capabilities & _HISTORY_CHOICE_REQUIRED
|
|
664
674
|
),
|
|
675
|
+
generation=generation,
|
|
665
676
|
))
|
|
666
677
|
continue
|
|
667
678
|
if output_kind is OutputKind.HISTORY_BATCH:
|
|
@@ -691,6 +702,7 @@ class ServerMessageDecoder:
|
|
|
691
702
|
width,
|
|
692
703
|
height,
|
|
693
704
|
raw_capabilities,
|
|
705
|
+
generation,
|
|
694
706
|
) = _HISTORY_PANE_METADATA.unpack(
|
|
695
707
|
raw[offset:offset + _HISTORY_PANE_METADATA.size]
|
|
696
708
|
)
|
|
@@ -729,6 +741,7 @@ class ServerMessageDecoder:
|
|
|
729
741
|
history_choice_required=bool(
|
|
730
742
|
raw_capabilities & _HISTORY_CHOICE_REQUIRED
|
|
731
743
|
),
|
|
744
|
+
generation=generation,
|
|
732
745
|
))
|
|
733
746
|
if offset != len(raw):
|
|
734
747
|
raise ValueError("trailing history batch data")
|
|
@@ -843,7 +856,7 @@ class ServerMessageDecoder:
|
|
|
843
856
|
|
|
844
857
|
|
|
845
858
|
class ScreenUpdateDecoder:
|
|
846
|
-
"""Compatibility view which ignores
|
|
859
|
+
"""Compatibility view which ignores v15 control-response messages."""
|
|
847
860
|
|
|
848
861
|
def __init__(self) -> None:
|
|
849
862
|
self._decoder = ServerMessageDecoder()
|
|
@@ -45,6 +45,7 @@ from railmux import (
|
|
|
45
45
|
__version__,
|
|
46
46
|
local_open,
|
|
47
47
|
restart_state,
|
|
48
|
+
tmux_ctl,
|
|
48
49
|
tmux_health,
|
|
49
50
|
tmux_server,
|
|
50
51
|
)
|
|
@@ -403,6 +404,7 @@ class _PaneGeometry:
|
|
|
403
404
|
transcript_source: str | None = None
|
|
404
405
|
transcript_backed: bool = False
|
|
405
406
|
claude_history_policy: str = "ask"
|
|
407
|
+
history_generation: int = 0
|
|
406
408
|
|
|
407
409
|
|
|
408
410
|
@dataclass(frozen=True)
|
|
@@ -421,6 +423,24 @@ _INFERRED_TRANSCRIPT_LIMIT = 8
|
|
|
421
423
|
_TRANSCRIPT_MAX_BYTES = 32 * 1024 * 1024
|
|
422
424
|
_SESSION_BINDING_OPTION = "@railmux_binding_v1"
|
|
423
425
|
_SWAP_OPTIONS = ("@railmux_swap_primary", "@railmux_swap_secondary")
|
|
426
|
+
_HISTORY_GENERATION_RE = re.compile(
|
|
427
|
+
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
428
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _history_generation(marker: str) -> int:
|
|
433
|
+
"""Map one validated provider UUID to a bounded opaque wire epoch."""
|
|
434
|
+
if not _HISTORY_GENERATION_RE.fullmatch(marker):
|
|
435
|
+
return 0
|
|
436
|
+
return int.from_bytes(
|
|
437
|
+
hashlib.blake2b(
|
|
438
|
+
marker.lower().encode("ascii"),
|
|
439
|
+
digest_size=8,
|
|
440
|
+
person=b"railmux-history",
|
|
441
|
+
).digest(),
|
|
442
|
+
"big",
|
|
443
|
+
)
|
|
424
444
|
|
|
425
445
|
|
|
426
446
|
_ANSI_FG = {
|
|
@@ -834,6 +854,7 @@ def _list_agent_panes(
|
|
|
834
854
|
"#{history_size} #{alternate_on} #{mouse_any_flag} "
|
|
835
855
|
f"#{{{tmux_server.HISTORY_SOURCE_OPTION}}} "
|
|
836
856
|
f"#{{{tmux_server.TRANSCRIPT_SOURCE_OPTION}}} "
|
|
857
|
+
f"#{{{tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION}}} "
|
|
837
858
|
f"#{{{TOOL_PANE_OPTION}}}",
|
|
838
859
|
),
|
|
839
860
|
stderr=subprocess.DEVNULL,
|
|
@@ -849,7 +870,7 @@ def _list_agent_panes(
|
|
|
849
870
|
for raw_row in output.splitlines():
|
|
850
871
|
fields = raw_row.split(" ")
|
|
851
872
|
if (
|
|
852
|
-
len(fields) !=
|
|
873
|
+
len(fields) != 17
|
|
853
874
|
or fields[0] != session_id
|
|
854
875
|
or fields[2] not in ("0", "1")
|
|
855
876
|
or fields[3] not in ("0", "1")
|
|
@@ -880,7 +901,7 @@ def _list_agent_panes(
|
|
|
880
901
|
):
|
|
881
902
|
return ()
|
|
882
903
|
seen.add(pane_id)
|
|
883
|
-
if is_tool_pane_marker(fields[
|
|
904
|
+
if is_tool_pane_marker(fields[16], outer_session_id=session_id):
|
|
884
905
|
continue
|
|
885
906
|
history_server = None
|
|
886
907
|
history_pane_id = None
|
|
@@ -931,6 +952,7 @@ def _list_agent_panes(
|
|
|
931
952
|
transcript_source=transcript_marker,
|
|
932
953
|
transcript_backed=transcript_backed,
|
|
933
954
|
claude_history_policy=claude_history_policy,
|
|
955
|
+
history_generation=_history_generation(fields[15]),
|
|
934
956
|
),
|
|
935
957
|
)
|
|
936
958
|
)
|
|
@@ -1376,6 +1398,7 @@ def _capture_pane_history(
|
|
|
1376
1398
|
else pane.history_size + pane.height > len(lines)
|
|
1377
1399
|
)
|
|
1378
1400
|
),
|
|
1401
|
+
generation=pane.history_generation,
|
|
1379
1402
|
)
|
|
1380
1403
|
|
|
1381
1404
|
|
|
@@ -1916,7 +1939,7 @@ def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
|
1916
1939
|
|
|
1917
1940
|
|
|
1918
1941
|
def terminal_modes_for_screen(screen: object) -> TerminalMode:
|
|
1919
|
-
"""Project pyte's private-mode set onto the bounded
|
|
1942
|
+
"""Project pyte's private-mode set onto the bounded v15 wire allowlist."""
|
|
1920
1943
|
terminal_modes = TerminalMode.NONE
|
|
1921
1944
|
if 2004 << 5 in screen.mode:
|
|
1922
1945
|
terminal_modes |= TerminalMode.BRACKETED_PASTE
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
|
+
import signal
|
|
11
12
|
import shlex
|
|
12
13
|
import shutil
|
|
13
14
|
import subprocess
|
|
@@ -302,6 +303,12 @@ _ROLLOUT_UUID_RE = re.compile(
|
|
|
302
303
|
)
|
|
303
304
|
|
|
304
305
|
|
|
306
|
+
def rollout_uuid_from_path(path: Path) -> str | None:
|
|
307
|
+
"""Return the filename UUID for a canonical Codex rollout path."""
|
|
308
|
+
match = _ROLLOUT_UUID_RE.search(path.name)
|
|
309
|
+
return match.group(1) if match is not None else None
|
|
310
|
+
|
|
311
|
+
|
|
305
312
|
def pane_pid_for_session(session_name: str) -> int | None:
|
|
306
313
|
"""Return the first pane's PID for *session_name*, or None.
|
|
307
314
|
|
|
@@ -1818,6 +1825,7 @@ _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
|
1818
1825
|
RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
|
|
1819
1826
|
RAILMUX_TARGET_OPTION = "@railmux_target_pane"
|
|
1820
1827
|
RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
|
|
1828
|
+
RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
|
|
1821
1829
|
# tmux names function keys only through F12. Private controller routes use
|
|
1822
1830
|
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1823
1831
|
# user input forwarded to an agent pane.
|
|
@@ -3347,6 +3355,47 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
|
|
|
3347
3355
|
)
|
|
3348
3356
|
|
|
3349
3357
|
|
|
3358
|
+
def reset_pane_history(identity: PaneIdentity) -> bool:
|
|
3359
|
+
"""Reset visible bytes and scrollback for the exact observed live pane.
|
|
3360
|
+
|
|
3361
|
+
Revalidate the immutable session, pane process, and window before issuing
|
|
3362
|
+
one tmux command queue that resets the terminal model and clears history.
|
|
3363
|
+
Then send the pane's foreground process group an ordinary same-size
|
|
3364
|
+
``SIGWINCH`` so its TUI repaints and reasserts terminal modes without a
|
|
3365
|
+
geometry change or injected keyboard input.
|
|
3366
|
+
"""
|
|
3367
|
+
current = pane_identity(identity.pane_id)
|
|
3368
|
+
if (current is None or current.dead
|
|
3369
|
+
or current.pane_pid != identity.pane_pid
|
|
3370
|
+
or current.session_id != identity.session_id
|
|
3371
|
+
or current.window_id != identity.window_id):
|
|
3372
|
+
return False
|
|
3373
|
+
try:
|
|
3374
|
+
subprocess.check_call(
|
|
3375
|
+
[
|
|
3376
|
+
"tmux", "send-keys", "-R", "-t", identity.pane_id,
|
|
3377
|
+
";", "clear-history", "-t", identity.pane_id,
|
|
3378
|
+
],
|
|
3379
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
3380
|
+
)
|
|
3381
|
+
except (OSError, subprocess.CalledProcessError):
|
|
3382
|
+
return False
|
|
3383
|
+
|
|
3384
|
+
current = pane_identity(identity.pane_id)
|
|
3385
|
+
if (current is None or current.dead
|
|
3386
|
+
or current.pane_pid != identity.pane_pid
|
|
3387
|
+
or current.session_id != identity.session_id
|
|
3388
|
+
or current.window_id != identity.window_id):
|
|
3389
|
+
return True
|
|
3390
|
+
try:
|
|
3391
|
+
os.killpg(os.getpgid(current.pane_pid), signal.SIGWINCH)
|
|
3392
|
+
except (OSError, ValueError):
|
|
3393
|
+
# The presentation cache is already clean. A provider that exited or
|
|
3394
|
+
# cannot be signalled will repaint on its next ordinary terminal write.
|
|
3395
|
+
pass
|
|
3396
|
+
return True
|
|
3397
|
+
|
|
3398
|
+
|
|
3350
3399
|
def kill_session_identity(identity: PaneIdentity) -> bool:
|
|
3351
3400
|
"""Kill an exact immutable tmux session, never a reused session name."""
|
|
3352
3401
|
current = pane_identity(identity.pane_id)
|