railmux 0.4.0.dev3__tar.gz → 0.4.0.dev4__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.4.0.dev3/src/railmux.egg-info → railmux-0.4.0.dev4}/PKG-INFO +25 -16
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/README.md +18 -10
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/pyproject.toml +6 -5
- railmux-0.4.0.dev4/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/codex_index.py +5 -3
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/config.py +2 -1
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/discovery.py +2 -2
- railmux-0.4.0.dev4/src/railmux/entrypoint.py +21 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/mouse_manager.py +8 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/path_codec.py +16 -0
- railmux-0.4.0.dev4/src/railmux/provider_paths.py +41 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/self_update.py +5 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_ctl.py +82 -7
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/transcript.py +83 -28
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/app.py +141 -15
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/modals.py +1 -1
- railmux-0.4.0.dev4/src/railmux/windows_bootstrap.py +174 -0
- railmux-0.4.0.dev4/src/railmux/windows_msys2.py +431 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4/src/railmux.egg-info}/PKG-INFO +25 -16
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/SOURCES.txt +4 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/requires.txt +7 -3
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_index.py +19 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_config.py +8 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_focus_highlight.py +135 -8
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_mouse_manager.py +17 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_path_codec.py +17 -1
- railmux-0.4.0.dev4/tests/test_provider_paths.py +29 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_release_workflow.py +5 -2
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_self_update.py +11 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_soft_quit.py +42 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_ctl.py +28 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_integration.py +1 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_transcript.py +84 -0
- railmux-0.4.0.dev4/tests/test_windows_bootstrap.py +143 -0
- railmux-0.4.0.dev4/tests/test_windows_msys2.py +216 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_windows_wrapper_contract.py +16 -11
- railmux-0.4.0.dev3/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev3/src/railmux/entrypoint.py +0 -26
- railmux-0.4.0.dev3/src/railmux/windows_bootstrap.py +0 -253
- railmux-0.4.0.dev3/tests/test_windows_bootstrap.py +0 -236
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/LICENSE +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/setup.cfg +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev4
|
|
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
|
|
@@ -14,6 +14,7 @@ Classifier: Environment :: Console :: Curses
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Operating System :: POSIX :: Linux
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
18
|
Classifier: Programming Language :: Python :: 3
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -26,12 +27,12 @@ Classifier: Topic :: Utilities
|
|
|
26
27
|
Requires-Python: >=3.9
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
28
29
|
License-File: LICENSE
|
|
29
|
-
Requires-Dist: packaging>=23.0
|
|
30
|
-
Requires-Dist: tomlkit<1,>=0.13.2
|
|
31
|
-
Requires-Dist: urwid>=2.6.16
|
|
32
|
-
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
30
|
+
Requires-Dist: packaging>=23.0; sys_platform != "win32"
|
|
31
|
+
Requires-Dist: tomlkit<1,>=0.13.2; sys_platform != "win32"
|
|
32
|
+
Requires-Dist: urwid>=2.6.16; sys_platform != "win32"
|
|
33
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11" and sys_platform != "win32"
|
|
33
34
|
Provides-Extra: ssh
|
|
34
|
-
Requires-Dist: pyte<0.9,>=0.8.2; extra == "ssh"
|
|
35
|
+
Requires-Dist: pyte<0.9,>=0.8.2; sys_platform != "win32" and extra == "ssh"
|
|
35
36
|
Provides-Extra: dev
|
|
36
37
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
37
38
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
@@ -169,12 +170,14 @@ for focus and layout limits.
|
|
|
169
170
|
|--------|--------|
|
|
170
171
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
171
172
|
| Left-click (running) | Switch the Target pane to that session |
|
|
173
|
+
| Wheel up over a live agent | Open its canonical current-branch history |
|
|
172
174
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
173
175
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
174
176
|
|
|
175
177
|
Unavailable context-menu actions are hidden for the selected session state.
|
|
176
|
-
Single-click and `␣` act in the Target pane without moving keyboard focus
|
|
177
|
-
|
|
178
|
+
Single-click and `␣` act in the Target pane without moving keyboard focus, but
|
|
179
|
+
`␣` always means history while a click on a running row means live attach.
|
|
180
|
+
Double-click and `Enter` also move focus there. The terminal must report mouse
|
|
178
181
|
buttons to applications for these actions to reach Railmux. Right-click
|
|
179
182
|
reporting is sometimes a separate setting from ordinary mouse reporting; see
|
|
180
183
|
[FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
|
|
@@ -207,7 +210,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
207
210
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
208
211
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
209
212
|
| `o` | Open persistent Railmux options |
|
|
210
|
-
| `␣` | Preview stopped or
|
|
213
|
+
| `␣` | Preview canonical history for the selected stopped or running session |
|
|
211
214
|
| `F8` | Cycle agent layout: single → side-by-side → stacked |
|
|
212
215
|
| `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
|
|
213
216
|
| `?` | Full help popup with all keybindings |
|
|
@@ -299,7 +302,8 @@ to single leaves Pane 2's agent running in the background.
|
|
|
299
302
|
|
|
300
303
|
In a split, focus an agent pane to make it the Target pane. After focus returns
|
|
301
304
|
to the sidebar, single-click or `␣` acts in that pane without moving keyboard
|
|
302
|
-
focus;
|
|
305
|
+
focus; a running-row click attaches live while `␣` previews history.
|
|
306
|
+
Double-click or `Enter` opens there and transfers focus. The status bar
|
|
303
307
|
at the bottom-left shows the current layout and Target pane:
|
|
304
308
|
|
|
305
309
|
| Symbol | Meaning |
|
|
@@ -363,15 +367,20 @@ sessions move ahead of the other results.
|
|
|
363
367
|
## History preview
|
|
364
368
|
|
|
365
369
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
366
|
-
the Target pane without starting or resuming the agent.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
+
the Target pane without starting or resuming the agent. For a running session,
|
|
371
|
+
press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
|
|
372
|
+
read-only: it cannot send a message or change the session. User and assistant
|
|
373
|
+
messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
374
|
+
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
375
|
+
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
376
|
+
conversation: the retained prefix and replacement suffix remain visible, while
|
|
377
|
+
the abandoned suffix is hidden.
|
|
370
378
|
|
|
371
379
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
372
380
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
373
|
-
matches, and `q` to exit
|
|
374
|
-
|
|
381
|
+
matches, and `q` to exit. If preview began from a live pane, `q` restores that
|
|
382
|
+
exact session; otherwise it restores the prior Target pane. Double-click to
|
|
383
|
+
skip preview and open the session directly.
|
|
375
384
|
|
|
376
385
|
## Status indicators
|
|
377
386
|
|
|
@@ -127,12 +127,14 @@ for focus and layout limits.
|
|
|
127
127
|
|--------|--------|
|
|
128
128
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
129
129
|
| Left-click (running) | Switch the Target pane to that session |
|
|
130
|
+
| Wheel up over a live agent | Open its canonical current-branch history |
|
|
130
131
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
131
132
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
132
133
|
|
|
133
134
|
Unavailable context-menu actions are hidden for the selected session state.
|
|
134
|
-
Single-click and `␣` act in the Target pane without moving keyboard focus
|
|
135
|
-
|
|
135
|
+
Single-click and `␣` act in the Target pane without moving keyboard focus, but
|
|
136
|
+
`␣` always means history while a click on a running row means live attach.
|
|
137
|
+
Double-click and `Enter` also move focus there. The terminal must report mouse
|
|
136
138
|
buttons to applications for these actions to reach Railmux. Right-click
|
|
137
139
|
reporting is sometimes a separate setting from ordinary mouse reporting; see
|
|
138
140
|
[FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
|
|
@@ -165,7 +167,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
165
167
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
166
168
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
167
169
|
| `o` | Open persistent Railmux options |
|
|
168
|
-
| `␣` | Preview stopped or
|
|
170
|
+
| `␣` | Preview canonical history for the selected stopped or running session |
|
|
169
171
|
| `F8` | Cycle agent layout: single → side-by-side → stacked |
|
|
170
172
|
| `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
|
|
171
173
|
| `?` | Full help popup with all keybindings |
|
|
@@ -257,7 +259,8 @@ to single leaves Pane 2's agent running in the background.
|
|
|
257
259
|
|
|
258
260
|
In a split, focus an agent pane to make it the Target pane. After focus returns
|
|
259
261
|
to the sidebar, single-click or `␣` acts in that pane without moving keyboard
|
|
260
|
-
focus;
|
|
262
|
+
focus; a running-row click attaches live while `␣` previews history.
|
|
263
|
+
Double-click or `Enter` opens there and transfers focus. The status bar
|
|
261
264
|
at the bottom-left shows the current layout and Target pane:
|
|
262
265
|
|
|
263
266
|
| Symbol | Meaning |
|
|
@@ -321,15 +324,20 @@ sessions move ahead of the other results.
|
|
|
321
324
|
## History preview
|
|
322
325
|
|
|
323
326
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
324
|
-
the Target pane without starting or resuming the agent.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
327
|
+
the Target pane without starting or resuming the agent. For a running session,
|
|
328
|
+
press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
|
|
329
|
+
read-only: it cannot send a message or change the session. User and assistant
|
|
330
|
+
messages, tool calls, and abbreviated tool output are colour-coded, while
|
|
331
|
+
internal context and encrypted reasoning are hidden. Codex rewind lineages and
|
|
332
|
+
Claude Code `parentUuid` branches are projected onto the provider's current
|
|
333
|
+
conversation: the retained prefix and replacement suffix remain visible, while
|
|
334
|
+
the abandoned suffix is hidden.
|
|
328
335
|
|
|
329
336
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
330
337
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
331
|
-
matches, and `q` to exit
|
|
332
|
-
|
|
338
|
+
matches, and `q` to exit. If preview began from a live pane, `q` restores that
|
|
339
|
+
exact session; otherwise it restores the prior Target pane. Double-click to
|
|
340
|
+
skip preview and open the session directly.
|
|
333
341
|
|
|
334
342
|
## Status indicators
|
|
335
343
|
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
21
|
"Operating System :: POSIX :: Linux",
|
|
22
22
|
"Operating System :: MacOS",
|
|
23
|
+
"Operating System :: Microsoft :: Windows",
|
|
23
24
|
"Programming Language :: Python :: 3",
|
|
24
25
|
"Programming Language :: Python :: 3.9",
|
|
25
26
|
"Programming Language :: Python :: 3.10",
|
|
@@ -31,15 +32,15 @@ classifiers = [
|
|
|
31
32
|
"Topic :: Utilities",
|
|
32
33
|
]
|
|
33
34
|
dependencies = [
|
|
34
|
-
"packaging>=23.0",
|
|
35
|
-
"tomlkit>=0.13.2,<1",
|
|
36
|
-
"urwid>=2.6.16",
|
|
37
|
-
"tomli>=2.0.1; python_version < '3.11'",
|
|
35
|
+
"packaging>=23.0; sys_platform != 'win32'",
|
|
36
|
+
"tomlkit>=0.13.2,<1; sys_platform != 'win32'",
|
|
37
|
+
"urwid>=2.6.16; sys_platform != 'win32'",
|
|
38
|
+
"tomli>=2.0.1; python_version < '3.11' and sys_platform != 'win32'",
|
|
38
39
|
]
|
|
39
40
|
|
|
40
41
|
[project.optional-dependencies]
|
|
41
42
|
ssh = [
|
|
42
|
-
"pyte>=0.8.2,<0.9",
|
|
43
|
+
"pyte>=0.8.2,<0.9; sys_platform != 'win32'",
|
|
43
44
|
]
|
|
44
45
|
dev = [
|
|
45
46
|
"pytest>=8.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev4"
|
|
@@ -25,6 +25,7 @@ from railmux.models import (
|
|
|
25
25
|
Project,
|
|
26
26
|
SessionMeta,
|
|
27
27
|
)
|
|
28
|
+
from railmux.provider_paths import provider_path
|
|
28
29
|
from railmux.renames import Renames
|
|
29
30
|
|
|
30
31
|
# Codex has no reliable provider-neutral signal that distinguishes a long
|
|
@@ -97,9 +98,10 @@ class HiddenCodexStatus:
|
|
|
97
98
|
def persistent_cache_path(codex_home: Path) -> Path:
|
|
98
99
|
"""Return a private cache path unique to one Codex sessions root."""
|
|
99
100
|
raw_base = os.environ.get("XDG_CACHE_HOME")
|
|
101
|
+
requested_base = provider_path(raw_base).expanduser() if raw_base else None
|
|
100
102
|
base = (
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
+
requested_base
|
|
104
|
+
if requested_base is not None and requested_base.is_absolute()
|
|
103
105
|
else Path.home() / ".cache"
|
|
104
106
|
)
|
|
105
107
|
try:
|
|
@@ -1036,7 +1038,7 @@ def _parse_codex_session(
|
|
|
1036
1038
|
cwd_str = payload.get("cwd")
|
|
1037
1039
|
if not isinstance(cwd_str, str) or not cwd_str.strip():
|
|
1038
1040
|
return None
|
|
1039
|
-
cwd =
|
|
1041
|
+
cwd = provider_path(cwd_str)
|
|
1040
1042
|
forked_from = payload.get("forked_from_id")
|
|
1041
1043
|
forked_from_id = (
|
|
1042
1044
|
forked_from
|
|
@@ -18,6 +18,7 @@ from railmux.setting_contracts import (
|
|
|
18
18
|
bounds_for,
|
|
19
19
|
choices_for,
|
|
20
20
|
)
|
|
21
|
+
from railmux.provider_paths import provider_path
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class ConfigError(ValueError):
|
|
@@ -47,7 +48,7 @@ class Config:
|
|
|
47
48
|
and relative paths are made absolute before a launched Codex changes to
|
|
48
49
|
its project cwd. The directory is not required to exist.
|
|
49
50
|
"""
|
|
50
|
-
path =
|
|
51
|
+
path = provider_path(self.codex_home).expanduser()
|
|
51
52
|
try:
|
|
52
53
|
return path.resolve()
|
|
53
54
|
except OSError:
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from railmux.atomic_file import atomic_write_text
|
|
9
9
|
from railmux.models import Project
|
|
10
|
-
from railmux.path_codec import decode
|
|
10
|
+
from railmux.path_codec import decode, is_encoded_project_name
|
|
11
11
|
from railmux.session_index import _looks_like_uuid, _scan_session
|
|
12
12
|
|
|
13
13
|
|
|
@@ -94,7 +94,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
94
94
|
for entry in scan:
|
|
95
95
|
if not entry.is_dir(follow_symlinks=False):
|
|
96
96
|
continue
|
|
97
|
-
if not entry.name
|
|
97
|
+
if not is_encoded_project_name(entry.name):
|
|
98
98
|
continue
|
|
99
99
|
seen.add(entry.name)
|
|
100
100
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Platform-safe public console entry point."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(
|
|
9
|
+
argv: Sequence[str] | None = None,
|
|
10
|
+
*,
|
|
11
|
+
platform_name: str = os.name,
|
|
12
|
+
) -> int:
|
|
13
|
+
"""Dispatch before importing modules that require a POSIX terminal."""
|
|
14
|
+
if platform_name == "nt":
|
|
15
|
+
from railmux.windows_bootstrap import main as windows_main
|
|
16
|
+
|
|
17
|
+
return windows_main(argv)
|
|
18
|
+
|
|
19
|
+
from railmux.cli import main as posix_main
|
|
20
|
+
|
|
21
|
+
return posix_main(list(argv) if argv is not None else None)
|
|
@@ -157,6 +157,14 @@ class RootWheelForwardingManager:
|
|
|
157
157
|
state["backup"], token=token)
|
|
158
158
|
self._remove_state()
|
|
159
159
|
state = None
|
|
160
|
+
elif not tmux_ctl.root_wheel_bindings_are_current(token):
|
|
161
|
+
# The ownership transaction can survive a soft upgrade
|
|
162
|
+
# in the same tmux pane. Refresh only an exactly-owned
|
|
163
|
+
# wrapper so the new pane-local history route becomes
|
|
164
|
+
# active without treating it as a user binding.
|
|
165
|
+
if not tmux_ctl.set_root_wheel_forwarding(
|
|
166
|
+
state["backup"], token):
|
|
167
|
+
return False
|
|
160
168
|
if state is not None:
|
|
161
169
|
state["owners"] = sorted(
|
|
162
170
|
{*state["owners"], self._owner_pane_id})
|
|
@@ -9,6 +9,7 @@ directory listings to recover segments with non-ASCII characters.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import functools
|
|
12
|
+
import re
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
|
|
@@ -21,6 +22,20 @@ from pathlib import Path
|
|
|
21
22
|
_exists_memo: dict[str, bool] = {}
|
|
22
23
|
_is_dir_memo: dict[str, bool] = {}
|
|
23
24
|
_iterdir_memo: dict[str, list[Path]] = {}
|
|
25
|
+
_WINDOWS_PROJECT_NAME = re.compile(r"^([A-Za-z])--")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_encoded_project_name(name: str) -> bool:
|
|
29
|
+
"""Return whether *name* is a POSIX or native-Windows Claude project key."""
|
|
30
|
+
return name.startswith("-") or _WINDOWS_PROJECT_NAME.match(name) is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_encoded_project_name(name: str) -> str:
|
|
34
|
+
"""Map Claude's ``C--Users-...`` form to MSYS2's ``-c-Users-...`` form."""
|
|
35
|
+
match = _WINDOWS_PROJECT_NAME.match(name)
|
|
36
|
+
if match is None:
|
|
37
|
+
return name
|
|
38
|
+
return f"-{match.group(1).lower()}-{name[3:]}"
|
|
24
39
|
|
|
25
40
|
|
|
26
41
|
def _m_exists(p: Path) -> bool:
|
|
@@ -200,6 +215,7 @@ def decode(encoded: str) -> Path:
|
|
|
200
215
|
filesystem scan — see ``_scan_recover`` — to recover segments whose
|
|
201
216
|
non-ASCII characters were replaced by dashes during encoding.
|
|
202
217
|
"""
|
|
218
|
+
encoded = normalize_encoded_project_name(encoded)
|
|
203
219
|
if not encoded.startswith("-"):
|
|
204
220
|
raise ValueError(f"encoded name must start with '-': {encoded!r}")
|
|
205
221
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Translate provider-owned native Windows paths inside the MSYS2 preview."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def running_in_windows_wrapper(environ: Mapping[str, str] = os.environ) -> bool:
|
|
14
|
+
"""Return whether Railmux is running in its native-Windows MSYS2 wrapper."""
|
|
15
|
+
return environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def provider_path(
|
|
19
|
+
raw: str,
|
|
20
|
+
*,
|
|
21
|
+
environ: Mapping[str, str] = os.environ,
|
|
22
|
+
) -> Path:
|
|
23
|
+
"""Return a POSIX-visible path for provider metadata.
|
|
24
|
+
|
|
25
|
+
Native providers write Windows paths into session metadata. Railmux runs
|
|
26
|
+
under MSYS2, where the same locations are mounted as ``/c/...`` (or
|
|
27
|
+
``//server/share/...`` for UNC paths). Outside the wrapper, preserve the
|
|
28
|
+
existing POSIX behavior exactly.
|
|
29
|
+
"""
|
|
30
|
+
if not running_in_windows_wrapper(environ):
|
|
31
|
+
return Path(raw)
|
|
32
|
+
|
|
33
|
+
match = _DRIVE_PATH.match(raw)
|
|
34
|
+
if match is not None:
|
|
35
|
+
drive, tail = match.groups()
|
|
36
|
+
return Path(f"/{drive.lower()}/{tail.replace(chr(92), '/')}")
|
|
37
|
+
|
|
38
|
+
if raw.startswith("\\\\"):
|
|
39
|
+
return Path("//" + raw[2:].replace("\\", "/"))
|
|
40
|
+
|
|
41
|
+
return Path(raw.replace("\\", "/"))
|
|
@@ -141,6 +141,11 @@ def maybe_upgrade_before_launch(
|
|
|
141
141
|
settings: Settings,
|
|
142
142
|
) -> None:
|
|
143
143
|
"""Check once in the outer launcher and restart after an accepted update."""
|
|
144
|
+
# Native Windows owns this private venv through the verified bootstrap.
|
|
145
|
+
# Updating only the inner copy would break its exact-version marker and
|
|
146
|
+
# could replace files while a tmux generation is still alive.
|
|
147
|
+
if os.environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2":
|
|
148
|
+
return
|
|
144
149
|
policy = settings.update_policy
|
|
145
150
|
if policy == "never":
|
|
146
151
|
return
|
|
@@ -1817,6 +1817,18 @@ _PREFIX_TARGET_KEY = "Tab"
|
|
|
1817
1817
|
_PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
1818
1818
|
RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
|
|
1819
1819
|
RAILMUX_TARGET_OPTION = "@railmux_target_pane"
|
|
1820
|
+
RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
|
|
1821
|
+
# tmux names function keys only through F12. Private controller routes use
|
|
1822
|
+
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1823
|
+
# user input forwarded to an agent pane.
|
|
1824
|
+
RAILMUX_HISTORY_PREVIEW_KEYS = {
|
|
1825
|
+
"primary": ("F21", "\x1b[35~"),
|
|
1826
|
+
"secondary": ("F22", "\x1b[36~"),
|
|
1827
|
+
}
|
|
1828
|
+
RAILMUX_HISTORY_RESTORE_KEYS = {
|
|
1829
|
+
"primary": ("F23", "\x1b[42~"),
|
|
1830
|
+
"secondary": ("F24", "\x1b[43~"),
|
|
1831
|
+
}
|
|
1820
1832
|
RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
|
|
1821
1833
|
RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
|
|
1822
1834
|
RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
|
|
@@ -2145,28 +2157,69 @@ def set_root_wheel_forwarding(
|
|
|
2145
2157
|
backup: RootWheelBindingBackup,
|
|
2146
2158
|
token: str,
|
|
2147
2159
|
) -> bool:
|
|
2148
|
-
"""
|
|
2160
|
+
"""Open canonical live history on wheel-up, otherwise preserve tmux input.
|
|
2149
2161
|
|
|
2150
2162
|
The literal marker makes crash recovery able to distinguish Railmux's
|
|
2151
2163
|
wrapper from a binding the user installed while Railmux was running.
|
|
2164
|
+
A pane-local primary/secondary marker routes the first live-agent wheel-up
|
|
2165
|
+
to Railmux's controller; the transcript viewer clears that marker, so its
|
|
2166
|
+
subsequent wheel events are forwarded normally.
|
|
2152
2167
|
Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
|
|
2153
2168
|
unbound by default and therefore has no false branch.
|
|
2154
2169
|
"""
|
|
2155
2170
|
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2156
|
-
|
|
2171
|
+
mouse_condition = (
|
|
2157
2172
|
"#{&&:#{mouse_any_flag},"
|
|
2158
2173
|
f"#{{==:{marker},{marker}}}}}"
|
|
2159
2174
|
)
|
|
2175
|
+
controller_present = f"#{{!=:#{{{RAILMUX_CONTROLLER_OPTION}}},}}"
|
|
2176
|
+
|
|
2177
|
+
def _preview_condition(slot_key: str) -> str:
|
|
2178
|
+
return (
|
|
2179
|
+
"#{&&:"
|
|
2180
|
+
f"#{{==:#{{{RAILMUX_HISTORY_PREVIEW_OPTION}}},{slot_key}}},"
|
|
2181
|
+
f"{controller_present}}}"
|
|
2182
|
+
)
|
|
2183
|
+
|
|
2184
|
+
def _send_preview(slot_key: str) -> str:
|
|
2185
|
+
_key, sequence = RAILMUX_HISTORY_PREVIEW_KEYS[slot_key]
|
|
2186
|
+
return (
|
|
2187
|
+
f"send-keys -l -t '#{{{RAILMUX_CONTROLLER_OPTION}}}' -- "
|
|
2188
|
+
f"'{sequence}'"
|
|
2189
|
+
)
|
|
2190
|
+
|
|
2160
2191
|
try:
|
|
2161
2192
|
for key in _ROOT_WHEEL_KEYS:
|
|
2193
|
+
binding = backup.get(key)
|
|
2194
|
+
fallback = _binding_command(binding) if binding is not None else ""
|
|
2195
|
+
mouse_fallback = (
|
|
2196
|
+
f"if-shell -F -t = '{mouse_condition}' 'send-keys -M'"
|
|
2197
|
+
)
|
|
2198
|
+
if fallback:
|
|
2199
|
+
mouse_fallback += f" '{fallback}'"
|
|
2162
2200
|
args = [
|
|
2163
2201
|
"tmux", "bind-key", "-T", "root", key,
|
|
2164
|
-
"if-shell", "-F", "-t", "=", condition,
|
|
2165
|
-
"send-keys -M",
|
|
2166
2202
|
]
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2203
|
+
if key == "WheelUpPane":
|
|
2204
|
+
secondary_fallback = (
|
|
2205
|
+
"if-shell -F -t = "
|
|
2206
|
+
f"'{_preview_condition('secondary')}' "
|
|
2207
|
+
f"\"{_send_preview('secondary')}\" "
|
|
2208
|
+
f"\"{mouse_fallback}\""
|
|
2209
|
+
)
|
|
2210
|
+
args.extend([
|
|
2211
|
+
"if-shell", "-F", "-t", "=",
|
|
2212
|
+
_preview_condition("primary"),
|
|
2213
|
+
_send_preview("primary"),
|
|
2214
|
+
secondary_fallback,
|
|
2215
|
+
])
|
|
2216
|
+
else:
|
|
2217
|
+
args.extend([
|
|
2218
|
+
"if-shell", "-F", "-t", "=", mouse_condition,
|
|
2219
|
+
"send-keys -M",
|
|
2220
|
+
])
|
|
2221
|
+
if fallback:
|
|
2222
|
+
args.append(fallback)
|
|
2170
2223
|
subprocess.check_call(
|
|
2171
2224
|
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
2172
2225
|
except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
|
|
@@ -2186,6 +2239,28 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
|
|
|
2186
2239
|
)
|
|
2187
2240
|
|
|
2188
2241
|
|
|
2242
|
+
def root_wheel_bindings_are_current(token: str) -> bool:
|
|
2243
|
+
"""Whether the owned wrapper includes canonical-history routing."""
|
|
2244
|
+
bindings = read_root_wheel_bindings()
|
|
2245
|
+
up = bindings.get("WheelUpPane")
|
|
2246
|
+
marker = f"{_ROOT_WHEEL_MARKER}-{token}"
|
|
2247
|
+
return (
|
|
2248
|
+
all(
|
|
2249
|
+
binding is not None
|
|
2250
|
+
and marker in binding
|
|
2251
|
+
and "mouse_any_flag" in binding
|
|
2252
|
+
and "send-keys -M" in binding
|
|
2253
|
+
for binding in bindings.values()
|
|
2254
|
+
)
|
|
2255
|
+
and up is not None
|
|
2256
|
+
and RAILMUX_HISTORY_PREVIEW_OPTION in up
|
|
2257
|
+
and all(
|
|
2258
|
+
sequence in up or sequence.replace("\x1b", r"\033") in up
|
|
2259
|
+
for _key, sequence in RAILMUX_HISTORY_PREVIEW_KEYS.values()
|
|
2260
|
+
)
|
|
2261
|
+
)
|
|
2262
|
+
|
|
2263
|
+
|
|
2189
2264
|
def root_wheel_binding_is_original_or_owned(
|
|
2190
2265
|
key: str,
|
|
2191
2266
|
binding: str | None,
|
|
@@ -295,6 +295,81 @@ def _render_claude_native_tool_results(
|
|
|
295
295
|
yield RESET
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
def _claude_current_branch(records: list[dict]) -> list[dict]:
|
|
299
|
+
"""Project an append-only Claude JSONL onto its current main branch.
|
|
300
|
+
|
|
301
|
+
Claude Code links semantic records with ``uuid`` / ``parentUuid``. Editing
|
|
302
|
+
or rewinding a prompt appends a second child instead of removing the old
|
|
303
|
+
suffix, so file order is not conversation order. The newest non-sidechain
|
|
304
|
+
UUID is the active tip; walking its parents retains the shared prefix and
|
|
305
|
+
the replacement suffix while excluding abandoned and sub-agent branches.
|
|
306
|
+
|
|
307
|
+
UUID-less metadata is retained for the normal renderer filters. If the
|
|
308
|
+
input is a bounded tail, walking stops safely at the first missing parent.
|
|
309
|
+
"""
|
|
310
|
+
by_uuid: dict[str, dict] = {}
|
|
311
|
+
tip: str | None = None
|
|
312
|
+
for record in records:
|
|
313
|
+
uuid = record.get("uuid")
|
|
314
|
+
if not isinstance(uuid, str) or not uuid:
|
|
315
|
+
continue
|
|
316
|
+
by_uuid[uuid] = record
|
|
317
|
+
if record.get("isSidechain") is not True:
|
|
318
|
+
tip = uuid
|
|
319
|
+
if tip is None:
|
|
320
|
+
return records
|
|
321
|
+
|
|
322
|
+
current: set[str] = set()
|
|
323
|
+
cursor: str | None = tip
|
|
324
|
+
while cursor and cursor not in current:
|
|
325
|
+
record = by_uuid.get(cursor)
|
|
326
|
+
if record is None:
|
|
327
|
+
break
|
|
328
|
+
current.add(cursor)
|
|
329
|
+
parent = record.get("parentUuid")
|
|
330
|
+
cursor = parent if isinstance(parent, str) and parent else None
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
record for record in records
|
|
334
|
+
if not isinstance(record.get("uuid"), str)
|
|
335
|
+
or not record.get("uuid")
|
|
336
|
+
or record["uuid"] in current
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _render_claude_records(
|
|
341
|
+
records: list[dict], *, claude_native: bool,
|
|
342
|
+
):
|
|
343
|
+
"""Render only the active Claude branch from buffered JSONL records."""
|
|
344
|
+
calls: dict[str, str] = {}
|
|
345
|
+
for record in _claude_current_branch(records):
|
|
346
|
+
rtype = record.get("type", "")
|
|
347
|
+
if rtype in _SKIP_TYPES:
|
|
348
|
+
continue
|
|
349
|
+
if _is_real_user(record):
|
|
350
|
+
rendered = (
|
|
351
|
+
_render_claude_native_user(record)
|
|
352
|
+
if claude_native
|
|
353
|
+
else _render_user(record)
|
|
354
|
+
)
|
|
355
|
+
if rendered:
|
|
356
|
+
yield rendered
|
|
357
|
+
if rtype == "user":
|
|
358
|
+
renderer = (
|
|
359
|
+
_render_claude_native_tool_results
|
|
360
|
+
if claude_native
|
|
361
|
+
else _render_claude_tool_results
|
|
362
|
+
)
|
|
363
|
+
yield from renderer(record, calls)
|
|
364
|
+
elif rtype == "assistant":
|
|
365
|
+
renderer = (
|
|
366
|
+
_render_claude_native_assistant
|
|
367
|
+
if claude_native
|
|
368
|
+
else _render_assistant_blocks
|
|
369
|
+
)
|
|
370
|
+
yield from renderer(record, calls)
|
|
371
|
+
|
|
372
|
+
|
|
298
373
|
def format_transcript(
|
|
299
374
|
source: Path | object,
|
|
300
375
|
fmt: str | None = None,
|
|
@@ -326,7 +401,10 @@ def format_transcript(
|
|
|
326
401
|
# call_id -> tool name, so Codex tool outputs can be labelled with the
|
|
327
402
|
# call that produced them (issue #3).
|
|
328
403
|
codex_calls: dict[str, str] = {}
|
|
329
|
-
|
|
404
|
+
# Claude rewind/edit branches live in one append-only JSONL, so those
|
|
405
|
+
# records must be buffered until the active tip is known. Production
|
|
406
|
+
# callers bound the input to the latest 2,000 records.
|
|
407
|
+
claude_records: list[dict] = []
|
|
330
408
|
try:
|
|
331
409
|
if isinstance(source, Path):
|
|
332
410
|
fh = open(source, "r", encoding="utf-8")
|
|
@@ -360,33 +438,10 @@ def format_transcript(
|
|
|
360
438
|
if _codex:
|
|
361
439
|
yield from _render_codex(record, codex_calls)
|
|
362
440
|
continue
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if _is_real_user(record):
|
|
369
|
-
rendered = (
|
|
370
|
-
_render_claude_native_user(record)
|
|
371
|
-
if claude_native
|
|
372
|
-
else _render_user(record)
|
|
373
|
-
)
|
|
374
|
-
if rendered:
|
|
375
|
-
yield rendered
|
|
376
|
-
if rtype == "user":
|
|
377
|
-
renderer = (
|
|
378
|
-
_render_claude_native_tool_results
|
|
379
|
-
if claude_native
|
|
380
|
-
else _render_claude_tool_results
|
|
381
|
-
)
|
|
382
|
-
yield from renderer(record, claude_calls)
|
|
383
|
-
elif rtype == "assistant":
|
|
384
|
-
renderer = (
|
|
385
|
-
_render_claude_native_assistant
|
|
386
|
-
if claude_native
|
|
387
|
-
else _render_assistant_blocks
|
|
388
|
-
)
|
|
389
|
-
yield from renderer(record, claude_calls)
|
|
441
|
+
claude_records.append(record)
|
|
442
|
+
if _codex is False:
|
|
443
|
+
yield from _render_claude_records(
|
|
444
|
+
claude_records, claude_native=claude_native)
|
|
390
445
|
except OSError:
|
|
391
446
|
yield f"{YELLOW}Could not read: {source}{RESET}\n"
|
|
392
447
|
finally:
|