railmux 0.4.0.dev2__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.dev2/src/railmux.egg-info → railmux-0.4.0.dev4}/PKG-INFO +24 -19
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/README.md +18 -10
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/pyproject.toml +6 -9
- railmux-0.4.0.dev4/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/__main__.py +1 -1
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/cli.py +2 -40
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/codex_index.py +20 -9
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/config.py +3 -4
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/discovery.py +7 -66
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/display_transport.py +18 -0
- railmux-0.4.0.dev4/src/railmux/entrypoint.py +21 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/fast_display_client.py +24 -11
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/fast_display_server.py +35 -8
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/favorites.py +1 -2
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/help_workspace.py +7 -2
- railmux-0.4.0.dev4/src/railmux/local_clipboard.py +47 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/local_open.py +10 -37
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/modes.py +0 -8
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/mouse_manager.py +11 -7
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/orphan_marker.py +14 -9
- {railmux-0.4.0.dev2 → 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.dev2 → railmux-0.4.0.dev4}/src/railmux/renames.py +1 -2
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/restart_state.py +12 -7
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/runtime_config.py +1 -4
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/scroll_manager.py +5 -3
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/self_update.py +5 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ssh_display_diagnostics.py +5 -3
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/tmux_binding_manager.py +3 -7
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/tmux_ctl.py +82 -7
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/transcript.py +83 -28
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/app.py +413 -350
- {railmux-0.4.0.dev2 → 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.dev2 → railmux-0.4.0.dev4/src/railmux.egg-info}/PKG-INFO +24 -19
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux.egg-info/SOURCES.txt +9 -36
- railmux-0.4.0.dev4/src/railmux.egg-info/entry_points.txt +2 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux.egg-info/requires.txt +7 -7
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_cli.py +0 -15
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_codex_delete.py +3 -7
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_codex_index.py +19 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_config.py +8 -1
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_discovery.py +0 -24
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_display_transport.py +21 -1
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_focus_highlight.py +135 -8
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_local_open.py +0 -26
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_modes.py +0 -2
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_mouse_manager.py +17 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_orphan_recovery.py +2 -2
- {railmux-0.4.0.dev2 → 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.dev2 → railmux-0.4.0.dev4}/tests/test_release_workflow.py +25 -17
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_runtime_config.py +0 -12
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_self_update.py +11 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_soft_quit.py +83 -2
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tmux_ctl.py +28 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tmux_integration.py +10 -2
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_transcript.py +84 -0
- railmux-0.4.0.dev4/tests/test_utf8_state.py +111 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_wheel_smoke.py +1 -6
- 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.dev4/tests/test_windows_wrapper_contract.py +73 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tools/wheel_smoke.py +6 -12
- railmux-0.4.0.dev2/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev2/src/railmux/local_clipboard.py +0 -101
- railmux-0.4.0.dev2/src/railmux/mux/__init__.py +0 -7
- railmux-0.4.0.dev2/src/railmux/mux/backend.py +0 -75
- railmux-0.4.0.dev2/src/railmux/mux/tmux_backend.py +0 -165
- railmux-0.4.0.dev2/src/railmux/platform/__init__.py +0 -44
- railmux-0.4.0.dev2/src/railmux/platform/config_paths.py +0 -30
- railmux-0.4.0.dev2/src/railmux/platform/console.py +0 -165
- railmux-0.4.0.dev2/src/railmux/platform/file_security.py +0 -34
- railmux-0.4.0.dev2/src/railmux/platform/filelock.py +0 -42
- railmux-0.4.0.dev2/src/railmux/platform/multiplex.py +0 -132
- railmux-0.4.0.dev2/src/railmux/platform/process.py +0 -21
- railmux-0.4.0.dev2/src/railmux/platform/runtime_paths.py +0 -75
- railmux-0.4.0.dev2/src/railmux/terminal_emulator.py +0 -180
- railmux-0.4.0.dev2/src/railmux/ui/status_chrome.py +0 -149
- railmux-0.4.0.dev2/src/railmux/winlocal/__init__.py +0 -2
- railmux-0.4.0.dev2/src/railmux/winlocal/backend.py +0 -1088
- railmux-0.4.0.dev2/src/railmux/winlocal/client.py +0 -299
- railmux-0.4.0.dev2/src/railmux/winlocal/compositor.py +0 -287
- railmux-0.4.0.dev2/src/railmux/winlocal/conpty.py +0 -200
- railmux-0.4.0.dev2/src/railmux/winlocal/daemon.py +0 -398
- railmux-0.4.0.dev2/src/railmux/winlocal/history_viewer.py +0 -93
- railmux-0.4.0.dev2/src/railmux/winlocal/ipc.py +0 -88
- railmux-0.4.0.dev2/src/railmux/winlocal/session_store.py +0 -96
- railmux-0.4.0.dev2/src/railmux/winlocal/virtual_screen.py +0 -105
- railmux-0.4.0.dev2/src/railmux.egg-info/entry_points.txt +0 -2
- railmux-0.4.0.dev2/tests/test_mux_backend.py +0 -60
- railmux-0.4.0.dev2/tests/test_platform.py +0 -248
- railmux-0.4.0.dev2/tests/test_status_chrome.py +0 -87
- railmux-0.4.0.dev2/tests/test_windows_parity_contract.py +0 -79
- railmux-0.4.0.dev2/tests/test_winlocal_backend.py +0 -577
- railmux-0.4.0.dev2/tests/test_winlocal_compositor.py +0 -67
- railmux-0.4.0.dev2/tests/test_winlocal_conpty.py +0 -60
- railmux-0.4.0.dev2/tests/test_winlocal_daemon.py +0 -203
- railmux-0.4.0.dev2/tests/test_winlocal_history_viewer.py +0 -43
- railmux-0.4.0.dev2/tests/test_winlocal_ipc.py +0 -39
- railmux-0.4.0.dev2/tests/test_winlocal_real_conpty.py +0 -94
- railmux-0.4.0.dev2/tests/test_winlocal_session_store.py +0 -42
- railmux-0.4.0.dev2/tests/test_winlocal_virtual_screen.py +0 -62
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/LICENSE +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/setup.cfg +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev2 → railmux-0.4.0.dev4}/tools/release_notes.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
|
|
@@ -21,21 +21,18 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
25
24
|
Classifier: Topic :: Software Development
|
|
26
25
|
Classifier: Topic :: Terminals
|
|
27
26
|
Classifier: Topic :: Utilities
|
|
28
27
|
Requires-Python: >=3.9
|
|
29
28
|
Description-Content-Type: text/markdown
|
|
30
29
|
License-File: LICENSE
|
|
31
|
-
Requires-Dist: packaging>=23.0
|
|
32
|
-
Requires-Dist: tomlkit<1,>=0.13.2
|
|
33
|
-
Requires-Dist: urwid>=2.6.16
|
|
34
|
-
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
35
|
-
Requires-Dist: pyte<0.9,>=0.8.2; sys_platform == "win32"
|
|
36
|
-
Requires-Dist: pywinpty<4,>=3.0.5; sys_platform == "win32"
|
|
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"
|
|
37
34
|
Provides-Extra: ssh
|
|
38
|
-
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"
|
|
39
36
|
Provides-Extra: dev
|
|
40
37
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
41
38
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
@@ -173,12 +170,14 @@ for focus and layout limits.
|
|
|
173
170
|
|--------|--------|
|
|
174
171
|
| Left-click (non-running) | Preview session history in the Target pane |
|
|
175
172
|
| Left-click (running) | Switch the Target pane to that session |
|
|
173
|
+
| Wheel up over a live agent | Open its canonical current-branch history |
|
|
176
174
|
| Double-click | Open/attach in the Target pane and move focus there |
|
|
177
175
|
| Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
|
|
178
176
|
|
|
179
177
|
Unavailable context-menu actions are hidden for the selected session state.
|
|
180
|
-
Single-click and `␣` act in the Target pane without moving keyboard focus
|
|
181
|
-
|
|
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
|
|
182
181
|
buttons to applications for these actions to reach Railmux. Right-click
|
|
183
182
|
reporting is sometimes a separate setting from ordinary mouse reporting; see
|
|
184
183
|
[FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
|
|
@@ -211,7 +210,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
|
|
|
211
210
|
| `T` | Return to that agent's managed Vim viewer, when open |
|
|
212
211
|
| `m` | Switch agent mode (Claude Code ⇄ Codex) |
|
|
213
212
|
| `o` | Open persistent Railmux options |
|
|
214
|
-
| `␣` | Preview stopped or
|
|
213
|
+
| `␣` | Preview canonical history for the selected stopped or running session |
|
|
215
214
|
| `F8` | Cycle agent layout: single → side-by-side → stacked |
|
|
216
215
|
| `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
|
|
217
216
|
| `?` | Full help popup with all keybindings |
|
|
@@ -303,7 +302,8 @@ to single leaves Pane 2's agent running in the background.
|
|
|
303
302
|
|
|
304
303
|
In a split, focus an agent pane to make it the Target pane. After focus returns
|
|
305
304
|
to the sidebar, single-click or `␣` acts in that pane without moving keyboard
|
|
306
|
-
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
|
|
307
307
|
at the bottom-left shows the current layout and Target pane:
|
|
308
308
|
|
|
309
309
|
| Symbol | Meaning |
|
|
@@ -367,15 +367,20 @@ sessions move ahead of the other results.
|
|
|
367
367
|
## History preview
|
|
368
368
|
|
|
369
369
|
For a stopped session, left-click or press `␣` to view conversation history in
|
|
370
|
-
the Target pane without starting or resuming the agent.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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.
|
|
374
378
|
|
|
375
379
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
376
380
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
377
|
-
matches, and `q` to exit
|
|
378
|
-
|
|
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.
|
|
379
384
|
|
|
380
385
|
## Status indicators
|
|
381
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
|
|
|
@@ -27,23 +27,20 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.11",
|
|
28
28
|
"Programming Language :: Python :: 3.12",
|
|
29
29
|
"Programming Language :: Python :: 3.13",
|
|
30
|
-
"Programming Language :: Python :: 3.14",
|
|
31
30
|
"Topic :: Software Development",
|
|
32
31
|
"Topic :: Terminals",
|
|
33
32
|
"Topic :: Utilities",
|
|
34
33
|
]
|
|
35
34
|
dependencies = [
|
|
36
|
-
"packaging>=23.0",
|
|
37
|
-
"tomlkit>=0.13.2,<1",
|
|
38
|
-
"urwid>=2.6.16",
|
|
39
|
-
"tomli>=2.0.1; python_version < '3.11'",
|
|
40
|
-
"pyte>=0.8.2,<0.9; sys_platform == 'win32'",
|
|
41
|
-
"pywinpty>=3.0.5,<4; sys_platform == 'win32'",
|
|
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'",
|
|
42
39
|
]
|
|
43
40
|
|
|
44
41
|
[project.optional-dependencies]
|
|
45
42
|
ssh = [
|
|
46
|
-
"pyte>=0.8.2,<0.9",
|
|
43
|
+
"pyte>=0.8.2,<0.9; sys_platform != 'win32'",
|
|
47
44
|
]
|
|
48
45
|
dev = [
|
|
49
46
|
"pytest>=8.0",
|
|
@@ -54,7 +51,7 @@ dev = [
|
|
|
54
51
|
]
|
|
55
52
|
|
|
56
53
|
[project.scripts]
|
|
57
|
-
railmux = "railmux.
|
|
54
|
+
railmux = "railmux.entrypoint:main"
|
|
58
55
|
|
|
59
56
|
[project.urls]
|
|
60
57
|
Homepage = "https://rightglow.github.io/Railmux/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev4"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import os
|
|
5
4
|
import shutil
|
|
6
5
|
import subprocess
|
|
7
6
|
import sys
|
|
7
|
+
import termios
|
|
8
8
|
import time
|
|
9
9
|
from dataclasses import replace
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from railmux import __version__
|
|
13
13
|
from railmux.config import ConfigError, default_config_path, load_config
|
|
14
|
+
from railmux.diagnostics import is_ssh_session, run_doctor
|
|
14
15
|
from railmux.pane_surface import render_startup_surface
|
|
15
|
-
from railmux.platform import python_support_error
|
|
16
16
|
from railmux.runtime_config import (
|
|
17
17
|
activate_runtime_environment,
|
|
18
18
|
check_executable,
|
|
@@ -37,29 +37,9 @@ _LOCAL_WATCHDOG_INTERVAL = 5.0
|
|
|
37
37
|
_LOCAL_WATCHDOG_FAILURES = 3
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def _native_windows() -> bool:
|
|
41
|
-
return os.name == "nt"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def is_ssh_session(environment: dict[str, str] | None = None) -> bool:
|
|
45
|
-
"""Lazy compatibility wrapper that keeps POSIX diagnostics import-clean."""
|
|
46
|
-
from railmux.diagnostics import is_ssh_session as detect
|
|
47
|
-
|
|
48
|
-
return detect(environment)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def run_doctor(*args: object, **kwargs: object) -> int:
|
|
52
|
-
"""Lazy compatibility wrapper for the established CLI test/repair hook."""
|
|
53
|
-
from railmux.diagnostics import run_doctor as doctor
|
|
54
|
-
|
|
55
|
-
return doctor(*args, **kwargs)
|
|
56
|
-
|
|
57
|
-
|
|
58
40
|
def _restore_terminal(attributes: list | None) -> None:
|
|
59
41
|
if attributes is None:
|
|
60
42
|
return
|
|
61
|
-
import termios
|
|
62
|
-
|
|
63
43
|
try:
|
|
64
44
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
65
45
|
except (OSError, termios.error):
|
|
@@ -87,8 +67,6 @@ def _run_tmux_client_with_watchdog(
|
|
|
87
67
|
"""Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
|
|
88
68
|
attributes = None
|
|
89
69
|
if sys.stdin.isatty():
|
|
90
|
-
import termios
|
|
91
|
-
|
|
92
70
|
try:
|
|
93
71
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
94
72
|
except (OSError, termios.error):
|
|
@@ -174,10 +152,6 @@ def _run_tmux_client_with_watchdog(
|
|
|
174
152
|
|
|
175
153
|
|
|
176
154
|
def main(argv: list[str] | None = None) -> int:
|
|
177
|
-
support_error = python_support_error()
|
|
178
|
-
if support_error is not None:
|
|
179
|
-
print(f"error: {support_error}", file=sys.stderr)
|
|
180
|
-
return 2
|
|
181
155
|
raw_args = list(sys.argv[1:] if argv is None else argv)
|
|
182
156
|
if raw_args and raw_args[0] == "config":
|
|
183
157
|
from railmux.config_cli import main as config_main
|
|
@@ -364,18 +338,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
364
338
|
file=sys.stderr,
|
|
365
339
|
)
|
|
366
340
|
return 2
|
|
367
|
-
if _native_windows():
|
|
368
|
-
# Native Windows owns no local tmux server. The detached daemon keeps
|
|
369
|
-
# ConPTY handles alive while this terminal remains a disposable client.
|
|
370
|
-
activate_runtime_environment(replace(config, tmux_binary="tmux"))
|
|
371
|
-
if not args.inside_tmux:
|
|
372
|
-
from railmux.self_update import maybe_upgrade_before_launch
|
|
373
|
-
from railmux.settings import Settings
|
|
374
|
-
|
|
375
|
-
maybe_upgrade_before_launch(raw_args, Settings())
|
|
376
|
-
from railmux.winlocal.client import main as native_main
|
|
377
|
-
|
|
378
|
-
return native_main(raw_args, config=config)
|
|
379
341
|
if config.tmux_binary != "tmux":
|
|
380
342
|
tmux_check = check_executable("tmux", config.tmux_binary)
|
|
381
343
|
if not tmux_check.valid:
|
|
@@ -13,21 +13,19 @@ import hashlib
|
|
|
13
13
|
import json
|
|
14
14
|
import math
|
|
15
15
|
import os
|
|
16
|
+
import stat
|
|
16
17
|
import time
|
|
17
18
|
from dataclasses import dataclass, replace
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
from railmux.atomic_file import atomic_write_text
|
|
21
|
-
from railmux.platform.file_security import (
|
|
22
|
-
prepare_private_directory,
|
|
23
|
-
private_regular_file,
|
|
24
|
-
)
|
|
25
22
|
from railmux.models import (
|
|
26
23
|
AttentionCategory,
|
|
27
24
|
AttentionState,
|
|
28
25
|
Project,
|
|
29
26
|
SessionMeta,
|
|
30
27
|
)
|
|
28
|
+
from railmux.provider_paths import provider_path
|
|
31
29
|
from railmux.renames import Renames
|
|
32
30
|
|
|
33
31
|
# Codex has no reliable provider-neutral signal that distinguishes a long
|
|
@@ -100,9 +98,10 @@ class HiddenCodexStatus:
|
|
|
100
98
|
def persistent_cache_path(codex_home: Path) -> Path:
|
|
101
99
|
"""Return a private cache path unique to one Codex sessions root."""
|
|
102
100
|
raw_base = os.environ.get("XDG_CACHE_HOME")
|
|
101
|
+
requested_base = provider_path(raw_base).expanduser() if raw_base else None
|
|
103
102
|
base = (
|
|
104
|
-
|
|
105
|
-
if
|
|
103
|
+
requested_base
|
|
104
|
+
if requested_base is not None and requested_base.is_absolute()
|
|
106
105
|
else Path.home() / ".cache"
|
|
107
106
|
)
|
|
108
107
|
try:
|
|
@@ -460,7 +459,9 @@ class CodexIndex:
|
|
|
460
459
|
try:
|
|
461
460
|
info = path.lstat()
|
|
462
461
|
if (
|
|
463
|
-
not
|
|
462
|
+
not stat.S_ISREG(info.st_mode)
|
|
463
|
+
or info.st_uid != os.getuid()
|
|
464
|
+
or info.st_mode & 0o077
|
|
464
465
|
or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
|
|
465
466
|
):
|
|
466
467
|
return
|
|
@@ -515,7 +516,17 @@ class CodexIndex:
|
|
|
515
516
|
path = self._cache_path
|
|
516
517
|
if path is None:
|
|
517
518
|
return False
|
|
518
|
-
|
|
519
|
+
parent = path.parent
|
|
520
|
+
try:
|
|
521
|
+
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
522
|
+
info = parent.lstat()
|
|
523
|
+
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid():
|
|
524
|
+
return False
|
|
525
|
+
if info.st_mode & 0o077:
|
|
526
|
+
os.chmod(parent, stat.S_IMODE(info.st_mode) & ~0o077)
|
|
527
|
+
return True
|
|
528
|
+
except OSError:
|
|
529
|
+
return False
|
|
519
530
|
|
|
520
531
|
def _save_persistent_cache(self) -> None:
|
|
521
532
|
path = self._cache_path
|
|
@@ -1027,7 +1038,7 @@ def _parse_codex_session(
|
|
|
1027
1038
|
cwd_str = payload.get("cwd")
|
|
1028
1039
|
if not isinstance(cwd_str, str) or not cwd_str.strip():
|
|
1029
1040
|
return None
|
|
1030
|
-
cwd =
|
|
1041
|
+
cwd = provider_path(cwd_str)
|
|
1031
1042
|
forked_from = payload.get("forked_from_id")
|
|
1032
1043
|
forked_from_id = (
|
|
1033
1044
|
forked_from
|
|
@@ -6,8 +6,6 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from railmux.platform.config_paths import default_config_path as _platform_config_path
|
|
10
|
-
|
|
11
9
|
try:
|
|
12
10
|
import tomllib
|
|
13
11
|
except ModuleNotFoundError: # Python 3.9-3.10
|
|
@@ -20,6 +18,7 @@ from railmux.setting_contracts import (
|
|
|
20
18
|
bounds_for,
|
|
21
19
|
choices_for,
|
|
22
20
|
)
|
|
21
|
+
from railmux.provider_paths import provider_path
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
class ConfigError(ValueError):
|
|
@@ -49,7 +48,7 @@ class Config:
|
|
|
49
48
|
and relative paths are made absolute before a launched Codex changes to
|
|
50
49
|
its project cwd. The directory is not required to exist.
|
|
51
50
|
"""
|
|
52
|
-
path =
|
|
51
|
+
path = provider_path(self.codex_home).expanduser()
|
|
53
52
|
try:
|
|
54
53
|
return path.resolve()
|
|
55
54
|
except OSError:
|
|
@@ -57,7 +56,7 @@ class Config:
|
|
|
57
56
|
|
|
58
57
|
|
|
59
58
|
def default_config_path() -> Path:
|
|
60
|
-
return
|
|
59
|
+
return Path.home() / ".config" / "railmux" / "config.toml"
|
|
61
60
|
|
|
62
61
|
|
|
63
62
|
def _table(data: dict[str, Any], name: str) -> dict[str, Any]:
|
|
@@ -7,8 +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
|
|
11
|
-
from railmux.platform.config_paths import config_dir
|
|
10
|
+
from railmux.path_codec import decode, is_encoded_project_name
|
|
12
11
|
from railmux.session_index import _looks_like_uuid, _scan_session
|
|
13
12
|
|
|
14
13
|
|
|
@@ -38,7 +37,7 @@ def _path_cache_file() -> Path:
|
|
|
38
37
|
Decoding an encoded project name is expensive on an NFS home (see
|
|
39
38
|
path_codec), and the mapping is stable, so we persist it across launches.
|
|
40
39
|
"""
|
|
41
|
-
return
|
|
40
|
+
return Path.home() / ".config" / "railmux" / "path-cache.json"
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def _load_path_cache() -> dict[str, str]:
|
|
@@ -95,7 +94,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
95
94
|
for entry in scan:
|
|
96
95
|
if not entry.is_dir(follow_symlinks=False):
|
|
97
96
|
continue
|
|
98
|
-
if
|
|
97
|
+
if not is_encoded_project_name(entry.name):
|
|
99
98
|
continue
|
|
100
99
|
seen.add(entry.name)
|
|
101
100
|
|
|
@@ -107,16 +106,10 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
107
106
|
if cached is not None and Path(cached).is_dir():
|
|
108
107
|
real_path = Path(cached)
|
|
109
108
|
else:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
else:
|
|
116
|
-
try:
|
|
117
|
-
decoded = decode(entry.name)
|
|
118
|
-
except ValueError:
|
|
119
|
-
pass
|
|
109
|
+
try:
|
|
110
|
+
decoded = decode(entry.name)
|
|
111
|
+
except ValueError:
|
|
112
|
+
decoded = None
|
|
120
113
|
# Skip projects whose original directory no longer exists on
|
|
121
114
|
# this machine (deleted or moved). They can't be launched —
|
|
122
115
|
# `cd` into a missing dir fails — so don't list them, and drop
|
|
@@ -163,58 +156,6 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
163
156
|
return results
|
|
164
157
|
|
|
165
158
|
|
|
166
|
-
def _windows_project_path_from_transcript(
|
|
167
|
-
claude_dir: Path,
|
|
168
|
-
encoded_name: str,
|
|
169
|
-
) -> Path | None:
|
|
170
|
-
"""Recover a Windows project cwd from provider-owned transcript metadata.
|
|
171
|
-
|
|
172
|
-
Drive letters and UNC roots do not fit the POSIX path backtracker. Claude
|
|
173
|
-
records the exact cwd in its JSONL, so validate that bounded value against
|
|
174
|
-
the enclosing encoded directory instead of guessing a Windows path.
|
|
175
|
-
"""
|
|
176
|
-
try:
|
|
177
|
-
candidates = sorted(
|
|
178
|
-
claude_dir.glob("*.jsonl"),
|
|
179
|
-
key=lambda path: path.stat().st_mtime_ns,
|
|
180
|
-
reverse=True,
|
|
181
|
-
)[:8]
|
|
182
|
-
except OSError:
|
|
183
|
-
return None
|
|
184
|
-
for path in candidates:
|
|
185
|
-
try:
|
|
186
|
-
with path.open("r", encoding="utf-8", errors="replace") as stream:
|
|
187
|
-
for _line_number in range(64):
|
|
188
|
-
line = stream.readline(64 * 1024 + 1)
|
|
189
|
-
if not line:
|
|
190
|
-
break
|
|
191
|
-
if len(line) > 64 * 1024:
|
|
192
|
-
continue
|
|
193
|
-
try:
|
|
194
|
-
record = json.loads(line)
|
|
195
|
-
except (json.JSONDecodeError, ValueError):
|
|
196
|
-
continue
|
|
197
|
-
raw_cwd = record.get("cwd") if isinstance(record, dict) else None
|
|
198
|
-
if (
|
|
199
|
-
not isinstance(raw_cwd, str)
|
|
200
|
-
or not raw_cwd
|
|
201
|
-
or len(raw_cwd) > 32768
|
|
202
|
-
or "\0" in raw_cwd
|
|
203
|
-
):
|
|
204
|
-
continue
|
|
205
|
-
candidate = Path(raw_cwd)
|
|
206
|
-
if (
|
|
207
|
-
candidate.is_absolute()
|
|
208
|
-
and candidate.is_dir()
|
|
209
|
-
and _claude_encode_path(str(candidate)).casefold()
|
|
210
|
-
== encoded_name.casefold()
|
|
211
|
-
):
|
|
212
|
-
return candidate
|
|
213
|
-
except OSError:
|
|
214
|
-
continue
|
|
215
|
-
return None
|
|
216
|
-
|
|
217
|
-
|
|
218
159
|
def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, float]:
|
|
219
160
|
"""Count sessions that pass every ``_scan_session`` filter and return the max
|
|
220
161
|
JSONL mtime. Uses ``_scan_session`` so the count exactly matches the
|
|
@@ -738,6 +738,22 @@ class AgentDisplayTransport:
|
|
|
738
738
|
def _with_phase(state: SwapState, phase: str) -> SwapState:
|
|
739
739
|
return SwapState(**{**asdict(state), "phase": phase})
|
|
740
740
|
|
|
741
|
+
@staticmethod
|
|
742
|
+
def _fit_home_to_display(state: SwapState) -> None:
|
|
743
|
+
"""Best-effort preserve the provider's visible geometry at home.
|
|
744
|
+
|
|
745
|
+
While a real pane is displayed, its named session contains only the
|
|
746
|
+
inert placeholder. Resize that detached home window to the real
|
|
747
|
+
pane's current size before swapping back. Otherwise background output
|
|
748
|
+
is formatted at the placeholder's older (often much narrower) launch
|
|
749
|
+
geometry and later appears as a narrow discontinuity in scrollback.
|
|
750
|
+
|
|
751
|
+
Geometry is presentation-only: failure must never weaken or abort the
|
|
752
|
+
identity-pinned return transaction.
|
|
753
|
+
"""
|
|
754
|
+
tmux_ctl.fit_session_to_pane(
|
|
755
|
+
state.agent_tmux_name, state.agent_pane_id)
|
|
756
|
+
|
|
741
757
|
def park(self, slot: AgentSlot) -> bool:
|
|
742
758
|
"""Move one displayed swap agent home without forgetting its slot.
|
|
743
759
|
|
|
@@ -764,6 +780,7 @@ class AgentDisplayTransport:
|
|
|
764
780
|
parking = self._with_phase(state, "parking")
|
|
765
781
|
if not _write_marker_pair(parking, slot.key):
|
|
766
782
|
return False
|
|
783
|
+
self._fit_home_to_display(state)
|
|
767
784
|
if not tmux_ctl.swap_panes(
|
|
768
785
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
769
786
|
_write_marker_pair(state, slot.key)
|
|
@@ -857,6 +874,7 @@ class AgentDisplayTransport:
|
|
|
857
874
|
return False
|
|
858
875
|
returning = SwapState(**{**asdict(state), "phase": "returning"})
|
|
859
876
|
_write_marker_pair(returning, slot.key)
|
|
877
|
+
self._fit_home_to_display(state)
|
|
860
878
|
if not tmux_ctl.swap_panes(
|
|
861
879
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
862
880
|
return False
|
|
@@ -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)
|