railmux 0.4.0.dev31__tar.gz → 0.4.0.dev32__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.dev31/src/railmux.egg-info → railmux-0.4.0.dev32}/PKG-INFO +1 -1
- railmux-0.4.0.dev32/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/app.py +27 -13
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_bootstrap.py +12 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_install_log.py +6 -1
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_msys2.py +339 -12
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_focus_highlight.py +2 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_sessions_pane.py +33 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_bootstrap.py +30 -1
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_install_log.py +12 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_msys2.py +183 -0
- railmux-0.4.0.dev31/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/LICENSE +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/README.md +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/pyproject.toml +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/setup.cfg +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev32"
|
|
@@ -2035,21 +2035,35 @@ class App:
|
|
|
2035
2035
|
|
|
2036
2036
|
# --- history preview (display pane shows a transcript, not an agent session) ---
|
|
2037
2037
|
|
|
2038
|
+
def _paint_session_row_click_now(self, session_id: str | None) -> None:
|
|
2039
|
+
"""Acknowledge a pointer selection before synchronous tmux work."""
|
|
2040
|
+
pane = getattr(self, "_sessions_pane", None)
|
|
2041
|
+
if pane is None:
|
|
2042
|
+
return
|
|
2043
|
+
pane.set_selected_session(session_id)
|
|
2044
|
+
loop = getattr(self, "_loop", None)
|
|
2045
|
+
if loop is not None:
|
|
2046
|
+
loop.draw_screen()
|
|
2047
|
+
|
|
2038
2048
|
def _on_session_row_preview(self, session: SessionMeta) -> None:
|
|
2039
2049
|
"""Apply click semantics after rechecking whether the row is live."""
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2050
|
+
self._paint_session_row_click_now(session.session_id)
|
|
2051
|
+
try:
|
|
2052
|
+
running = self._by_session_id(session.session_id)
|
|
2053
|
+
if (running is not None
|
|
2054
|
+
and self._agent_session_alive(running.tmux_name)):
|
|
2055
|
+
self._on_running_select(
|
|
2056
|
+
RunningEntry(
|
|
2057
|
+
tmux_name=running.tmux_name,
|
|
2058
|
+
label=running.label,
|
|
2059
|
+
status=running.status,
|
|
2060
|
+
),
|
|
2061
|
+
steal_focus=False,
|
|
2062
|
+
)
|
|
2063
|
+
return
|
|
2064
|
+
self._on_session_preview(session)
|
|
2065
|
+
finally:
|
|
2066
|
+
self._paint_session_row_click_now(None)
|
|
2053
2067
|
|
|
2054
2068
|
def _on_session_preview(self, session: SessionMeta) -> None:
|
|
2055
2069
|
"""Show the provider's canonical current-branch history.
|
|
@@ -20,6 +20,7 @@ from railmux.windows_msys2 import (
|
|
|
20
20
|
plan_managed_runtime_prune,
|
|
21
21
|
reusable_managed_base_candidate,
|
|
22
22
|
)
|
|
23
|
+
from railmux.windows_paths import managed_windows_data_root
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
_MINIMUM_WINDOWS_PYTHON = (3, 10)
|
|
@@ -49,6 +50,14 @@ def _runtime_status(
|
|
|
49
50
|
) -> int:
|
|
50
51
|
snapshot = managed_runtime_status(
|
|
51
52
|
version=__version__, environ=environ, verify=verify)
|
|
53
|
+
data_root = (
|
|
54
|
+
managed_windows_data_root(environ)
|
|
55
|
+
if runtime is None or runtime.managed
|
|
56
|
+
else None
|
|
57
|
+
)
|
|
58
|
+
if data_root is not None:
|
|
59
|
+
snapshot["data_root"] = str(data_root)
|
|
60
|
+
snapshot["log_directory"] = str(data_root / "logs")
|
|
52
61
|
if json_output:
|
|
53
62
|
json.dump(snapshot, sys.stdout, indent=2, sort_keys=True)
|
|
54
63
|
print()
|
|
@@ -75,6 +84,9 @@ def _runtime_status(
|
|
|
75
84
|
if root is not None:
|
|
76
85
|
print(f"Managed location: {root}")
|
|
77
86
|
print("Next: run 'railmux runtime install' from an interactive PowerShell.")
|
|
87
|
+
if data_root is not None:
|
|
88
|
+
print(f"Data root: {data_root}")
|
|
89
|
+
print(f"Install logs: {data_root / 'logs'}")
|
|
78
90
|
print("Provider data: shared from the Windows user profile")
|
|
79
91
|
content = snapshot.get("content_identity")
|
|
80
92
|
if isinstance(content, str):
|
|
@@ -162,7 +162,12 @@ class InstallReporter:
|
|
|
162
162
|
|
|
163
163
|
def __enter__(self) -> InstallReporter:
|
|
164
164
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
-
|
|
165
|
+
# Windows PowerShell 5 treats BOM-less UTF-8 as the active ANSI code
|
|
166
|
+
# page. The signature keeps Chinese systems from rendering the
|
|
167
|
+
# middle dot/ellipsis as mojibake while remaining ordinary UTF-8 for
|
|
168
|
+
# modern readers (use ``utf-8-sig`` when the exact first character
|
|
169
|
+
# matters).
|
|
170
|
+
self._log = self.path.open("x", encoding="utf-8-sig", newline="\n")
|
|
166
171
|
self._write_log("Railmux Windows runtime installation\n")
|
|
167
172
|
# Windows may defer the visible last-write timestamp of an open file.
|
|
168
173
|
# Never let retention mistake the active evidence log for an old one.
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import concurrent.futures
|
|
10
10
|
import hashlib
|
|
11
11
|
import http.client
|
|
12
|
+
import importlib.metadata as importlib_metadata
|
|
12
13
|
import json
|
|
13
14
|
import lzma
|
|
14
15
|
import os
|
|
@@ -160,6 +161,22 @@ _PACKAGE_COMMAND = (
|
|
|
160
161
|
'--cache-dir "$cache" --only-binary=:all: '
|
|
161
162
|
'--timeout "$4" --retries "$5" "railmux[ssh]==$2"'
|
|
162
163
|
)
|
|
164
|
+
_LOCAL_APP_CHECK_COMMAND = (
|
|
165
|
+
'chmod 755 "$1/venv/bin/railmux" && '
|
|
166
|
+
'"$1/venv/bin/python" -m pip check && '
|
|
167
|
+
'"$1/venv/bin/python" -c '
|
|
168
|
+
"'import packaging, pyte, tomlkit, typing_extensions, urwid, wcwidth, "
|
|
169
|
+
"railmux, sys; assert railmux.__version__ == sys.argv[1]' \"$2\""
|
|
170
|
+
)
|
|
171
|
+
_LOCAL_APP_COPY_FILE_LIMIT = 20_000
|
|
172
|
+
_LOCAL_APP_COPY_SIZE_LIMIT = 128 * 1024 * 1024
|
|
173
|
+
_DEPENDENCY_SEED_NAMES = frozenset(
|
|
174
|
+
{"packaging", "pyte", "tomlkit", "typing_extensions.py", "urwid", "wcwidth"}
|
|
175
|
+
)
|
|
176
|
+
_DEPENDENCY_DIST_INFO_RE = re.compile(
|
|
177
|
+
r"(?:packaging|pyte|tomlkit|typing_extensions|urwid|wcwidth)-.+\.dist-info\Z",
|
|
178
|
+
re.IGNORECASE,
|
|
179
|
+
)
|
|
163
180
|
|
|
164
181
|
|
|
165
182
|
class RuntimeErrorBase(RuntimeError):
|
|
@@ -2454,6 +2471,289 @@ def _validate_managed_application_root(root: Path) -> None:
|
|
|
2454
2471
|
)
|
|
2455
2472
|
|
|
2456
2473
|
|
|
2474
|
+
def _venv_site_packages(application: Path) -> Path | None:
|
|
2475
|
+
"""Return one ordinary MSYS2 venv site-packages directory."""
|
|
2476
|
+
library = application / "venv" / "lib"
|
|
2477
|
+
try:
|
|
2478
|
+
candidates = list(library.glob("python*/site-packages"))
|
|
2479
|
+
except OSError:
|
|
2480
|
+
return None
|
|
2481
|
+
valid: list[Path] = []
|
|
2482
|
+
for candidate in candidates:
|
|
2483
|
+
try:
|
|
2484
|
+
if _path_is_link_or_reparse(candidate) or not candidate.is_dir():
|
|
2485
|
+
continue
|
|
2486
|
+
except OSError:
|
|
2487
|
+
continue
|
|
2488
|
+
valid.append(candidate)
|
|
2489
|
+
return valid[0] if len(valid) == 1 else None
|
|
2490
|
+
|
|
2491
|
+
|
|
2492
|
+
def _native_application_payload(version: str) -> tuple[Path, Path] | None:
|
|
2493
|
+
"""Locate the currently executing pure-Python package and dist-info."""
|
|
2494
|
+
try:
|
|
2495
|
+
distribution = importlib_metadata.distribution("railmux")
|
|
2496
|
+
except importlib_metadata.PackageNotFoundError:
|
|
2497
|
+
return None
|
|
2498
|
+
if distribution.version != version:
|
|
2499
|
+
return None
|
|
2500
|
+
package = Path(__file__).resolve().parent
|
|
2501
|
+
if not package.is_dir():
|
|
2502
|
+
return None
|
|
2503
|
+
info_names = {
|
|
2504
|
+
Path(str(item)).parts[0]
|
|
2505
|
+
for item in distribution.files or ()
|
|
2506
|
+
if Path(str(item)).parts
|
|
2507
|
+
and Path(str(item)).parts[0].casefold().startswith("railmux-")
|
|
2508
|
+
and Path(str(item)).parts[0].casefold().endswith(".dist-info")
|
|
2509
|
+
}
|
|
2510
|
+
if len(info_names) != 1:
|
|
2511
|
+
return None
|
|
2512
|
+
info = Path(distribution.locate_file(next(iter(info_names))))
|
|
2513
|
+
if not info.is_dir():
|
|
2514
|
+
return None
|
|
2515
|
+
return package, info
|
|
2516
|
+
|
|
2517
|
+
|
|
2518
|
+
def _copy_local_tree(
|
|
2519
|
+
source: Path,
|
|
2520
|
+
destination: Path,
|
|
2521
|
+
*,
|
|
2522
|
+
budget: list[int],
|
|
2523
|
+
ignored_names: frozenset[str] = frozenset(),
|
|
2524
|
+
) -> None:
|
|
2525
|
+
"""Copy one bounded tree without following links or reparse points."""
|
|
2526
|
+
try:
|
|
2527
|
+
if _path_is_link_or_reparse(source) or not source.is_dir():
|
|
2528
|
+
raise RuntimeInstallError("a local application seed tree was unsafe")
|
|
2529
|
+
except OSError as exc:
|
|
2530
|
+
raise RuntimeInstallError(
|
|
2531
|
+
"a local application seed tree could not be inspected"
|
|
2532
|
+
) from exc
|
|
2533
|
+
destination.mkdir()
|
|
2534
|
+
for current_text, directories, files in os.walk(source, followlinks=False):
|
|
2535
|
+
current = Path(current_text)
|
|
2536
|
+
relative = current.relative_to(source)
|
|
2537
|
+
target_root = destination / relative
|
|
2538
|
+
kept_directories: list[str] = []
|
|
2539
|
+
for name in directories:
|
|
2540
|
+
if name in ignored_names or name == "__pycache__":
|
|
2541
|
+
continue
|
|
2542
|
+
child = current / name
|
|
2543
|
+
try:
|
|
2544
|
+
if _path_is_link_or_reparse(child) or not child.is_dir():
|
|
2545
|
+
raise RuntimeInstallError(
|
|
2546
|
+
"a local application seed contained an unsafe directory"
|
|
2547
|
+
)
|
|
2548
|
+
except OSError as exc:
|
|
2549
|
+
raise RuntimeInstallError(
|
|
2550
|
+
"a local application seed could not be inspected"
|
|
2551
|
+
) from exc
|
|
2552
|
+
kept_directories.append(name)
|
|
2553
|
+
(target_root / name).mkdir()
|
|
2554
|
+
directories[:] = kept_directories
|
|
2555
|
+
for name in files:
|
|
2556
|
+
if (
|
|
2557
|
+
name in ignored_names
|
|
2558
|
+
or name.endswith((".pyc", ".pyo"))
|
|
2559
|
+
or name.endswith((".pth", ".egg-link"))
|
|
2560
|
+
):
|
|
2561
|
+
continue
|
|
2562
|
+
child = current / name
|
|
2563
|
+
try:
|
|
2564
|
+
info = child.lstat()
|
|
2565
|
+
if (
|
|
2566
|
+
_path_is_link_or_reparse(child)
|
|
2567
|
+
or not stat.S_ISREG(info.st_mode)
|
|
2568
|
+
):
|
|
2569
|
+
raise RuntimeInstallError(
|
|
2570
|
+
"a local application seed contained an unsafe file"
|
|
2571
|
+
)
|
|
2572
|
+
except OSError as exc:
|
|
2573
|
+
raise RuntimeInstallError(
|
|
2574
|
+
"a local application seed could not be inspected"
|
|
2575
|
+
) from exc
|
|
2576
|
+
budget[0] += 1
|
|
2577
|
+
budget[1] += info.st_size
|
|
2578
|
+
if (
|
|
2579
|
+
budget[0] > _LOCAL_APP_COPY_FILE_LIMIT
|
|
2580
|
+
or budget[1] > _LOCAL_APP_COPY_SIZE_LIMIT
|
|
2581
|
+
):
|
|
2582
|
+
raise RuntimeInstallError(
|
|
2583
|
+
"the local application seed exceeded its safety bound"
|
|
2584
|
+
)
|
|
2585
|
+
# Do not carry an NTFS read-only attribute from a packaged-Python
|
|
2586
|
+
# cache or an older app layer into the new writable venv.
|
|
2587
|
+
shutil.copyfile(child, target_root / name)
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
def _copy_local_file(source: Path, destination: Path, *, budget: list[int]) -> None:
|
|
2591
|
+
try:
|
|
2592
|
+
info = source.lstat()
|
|
2593
|
+
if _path_is_link_or_reparse(source) or not stat.S_ISREG(info.st_mode):
|
|
2594
|
+
raise RuntimeInstallError("a local dependency seed file was unsafe")
|
|
2595
|
+
except OSError as exc:
|
|
2596
|
+
raise RuntimeInstallError(
|
|
2597
|
+
"a local dependency seed file could not be inspected"
|
|
2598
|
+
) from exc
|
|
2599
|
+
budget[0] += 1
|
|
2600
|
+
budget[1] += info.st_size
|
|
2601
|
+
if (
|
|
2602
|
+
budget[0] > _LOCAL_APP_COPY_FILE_LIMIT
|
|
2603
|
+
or budget[1] > _LOCAL_APP_COPY_SIZE_LIMIT
|
|
2604
|
+
):
|
|
2605
|
+
raise RuntimeInstallError("the local application seed exceeded its safety bound")
|
|
2606
|
+
shutil.copyfile(source, destination)
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
def _dependency_seed_entries(site_packages: Path) -> list[Path] | None:
|
|
2610
|
+
try:
|
|
2611
|
+
entries = list(site_packages.iterdir())
|
|
2612
|
+
except OSError:
|
|
2613
|
+
return None
|
|
2614
|
+
selected = [
|
|
2615
|
+
entry
|
|
2616
|
+
for entry in entries
|
|
2617
|
+
if entry.name in _DEPENDENCY_SEED_NAMES
|
|
2618
|
+
or _DEPENDENCY_DIST_INFO_RE.fullmatch(entry.name)
|
|
2619
|
+
]
|
|
2620
|
+
names = {entry.name for entry in selected}
|
|
2621
|
+
if not _DEPENDENCY_SEED_NAMES.issubset(names):
|
|
2622
|
+
return None
|
|
2623
|
+
for dependency in _DEPENDENCY_SEED_NAMES - {"typing_extensions.py"}:
|
|
2624
|
+
if not any(
|
|
2625
|
+
name.casefold().startswith(f"{dependency.casefold()}-")
|
|
2626
|
+
and name.casefold().endswith(".dist-info")
|
|
2627
|
+
for name in names
|
|
2628
|
+
):
|
|
2629
|
+
return None
|
|
2630
|
+
if not any(
|
|
2631
|
+
name.casefold().startswith("typing_extensions-")
|
|
2632
|
+
and name.casefold().endswith(".dist-info")
|
|
2633
|
+
for name in names
|
|
2634
|
+
):
|
|
2635
|
+
return None
|
|
2636
|
+
return selected
|
|
2637
|
+
|
|
2638
|
+
|
|
2639
|
+
def _verified_dependency_seed(
|
|
2640
|
+
root: Path,
|
|
2641
|
+
*,
|
|
2642
|
+
version: str,
|
|
2643
|
+
environ: Mapping[str, str],
|
|
2644
|
+
probe: Probe,
|
|
2645
|
+
) -> tuple[str, Path] | None:
|
|
2646
|
+
marked = _marked_app_versions(root)
|
|
2647
|
+
if not marked:
|
|
2648
|
+
return None
|
|
2649
|
+
for candidate_version in sorted(marked, key=_version_key, reverse=True):
|
|
2650
|
+
if candidate_version == version:
|
|
2651
|
+
continue
|
|
2652
|
+
app_name = _app_name(candidate_version)
|
|
2653
|
+
candidate = Msys2Runtime(root, managed=False, app_name=app_name)
|
|
2654
|
+
if not probe_runtime(
|
|
2655
|
+
candidate,
|
|
2656
|
+
version=candidate_version,
|
|
2657
|
+
environ=environ,
|
|
2658
|
+
probe=probe,
|
|
2659
|
+
):
|
|
2660
|
+
continue
|
|
2661
|
+
site_packages = _venv_site_packages(marked[candidate_version])
|
|
2662
|
+
if (
|
|
2663
|
+
site_packages is not None
|
|
2664
|
+
and _dependency_seed_entries(site_packages) is not None
|
|
2665
|
+
):
|
|
2666
|
+
return candidate_version, site_packages
|
|
2667
|
+
return None
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
def _install_application_from_local_seed(
|
|
2671
|
+
root: Path,
|
|
2672
|
+
*,
|
|
2673
|
+
final_app: Path,
|
|
2674
|
+
final_posix: str,
|
|
2675
|
+
version: str,
|
|
2676
|
+
environ: Mapping[str, str],
|
|
2677
|
+
reporter: InstallReporter,
|
|
2678
|
+
runner: Runner | None,
|
|
2679
|
+
probe: Probe,
|
|
2680
|
+
) -> str:
|
|
2681
|
+
"""Build an upgrade layer locally; return ready, unavailable, or failed."""
|
|
2682
|
+
payload = _native_application_payload(version)
|
|
2683
|
+
seed = _verified_dependency_seed(
|
|
2684
|
+
root, version=version, environ=environ, probe=probe)
|
|
2685
|
+
target_site = _venv_site_packages(final_app)
|
|
2686
|
+
if payload is None or seed is None or target_site is None:
|
|
2687
|
+
return "unavailable"
|
|
2688
|
+
source_package, source_info = payload
|
|
2689
|
+
seed_version, seed_site = seed
|
|
2690
|
+
seed_entries = _dependency_seed_entries(seed_site)
|
|
2691
|
+
if seed_entries is None:
|
|
2692
|
+
return "unavailable"
|
|
2693
|
+
budget = [0, 0]
|
|
2694
|
+
try:
|
|
2695
|
+
for source in seed_entries:
|
|
2696
|
+
destination = target_site / source.name
|
|
2697
|
+
if destination.exists() or destination.is_symlink():
|
|
2698
|
+
raise RuntimeInstallError(
|
|
2699
|
+
"the new application environment unexpectedly contained "
|
|
2700
|
+
f"{source.name}"
|
|
2701
|
+
)
|
|
2702
|
+
if source.is_dir():
|
|
2703
|
+
_copy_local_tree(source, destination, budget=budget)
|
|
2704
|
+
else:
|
|
2705
|
+
_copy_local_file(source, destination, budget=budget)
|
|
2706
|
+
_copy_local_tree(
|
|
2707
|
+
source_package,
|
|
2708
|
+
target_site / "railmux",
|
|
2709
|
+
budget=budget,
|
|
2710
|
+
)
|
|
2711
|
+
_copy_local_tree(
|
|
2712
|
+
source_info,
|
|
2713
|
+
target_site / source_info.name,
|
|
2714
|
+
budget=budget,
|
|
2715
|
+
ignored_names=frozenset(
|
|
2716
|
+
{"RECORD", "direct_url.json", "INSTALLER", "REQUESTED"}
|
|
2717
|
+
),
|
|
2718
|
+
)
|
|
2719
|
+
entrypoint = final_app / "venv" / "bin" / "railmux"
|
|
2720
|
+
with entrypoint.open("x", encoding="utf-8", newline="\n") as stream:
|
|
2721
|
+
stream.write(
|
|
2722
|
+
f"#!{final_posix}/venv/bin/python\n"
|
|
2723
|
+
"from railmux.entrypoint import main\n"
|
|
2724
|
+
"raise SystemExit(main())\n"
|
|
2725
|
+
)
|
|
2726
|
+
except (OSError, shutil.Error, RuntimeInstallError) as exc:
|
|
2727
|
+
reporter.note(
|
|
2728
|
+
"Verified local app-layer reuse was unavailable "
|
|
2729
|
+
f"({exc}); falling back to the private PyPI cache.",
|
|
2730
|
+
level="warning",
|
|
2731
|
+
)
|
|
2732
|
+
return "failed"
|
|
2733
|
+
returncode = _run_checked(
|
|
2734
|
+
_bash_command(root, _LOCAL_APP_CHECK_COMMAND, final_posix, version),
|
|
2735
|
+
env=Msys2Runtime(root, managed=False).environment(environ),
|
|
2736
|
+
reporter=reporter,
|
|
2737
|
+
runner=runner,
|
|
2738
|
+
label="Local Railmux application validation",
|
|
2739
|
+
allow_failure=True,
|
|
2740
|
+
)
|
|
2741
|
+
if returncode:
|
|
2742
|
+
reporter.note(
|
|
2743
|
+
"The locally reused dependency layer did not satisfy the current "
|
|
2744
|
+
"package contract; falling back to the private PyPI cache.",
|
|
2745
|
+
level="warning",
|
|
2746
|
+
)
|
|
2747
|
+
return "failed"
|
|
2748
|
+
reporter.note(
|
|
2749
|
+
f"Reused verified dependencies from Railmux {seed_version}; installed "
|
|
2750
|
+
f"Railmux {version} from the current Windows package without network "
|
|
2751
|
+
"access.",
|
|
2752
|
+
level="success",
|
|
2753
|
+
)
|
|
2754
|
+
return "ready"
|
|
2755
|
+
|
|
2756
|
+
|
|
2457
2757
|
def _install_application(
|
|
2458
2758
|
root: Path,
|
|
2459
2759
|
*,
|
|
@@ -2534,21 +2834,48 @@ def _install_application(
|
|
|
2534
2834
|
runner=runner,
|
|
2535
2835
|
label="Railmux virtual environment creation",
|
|
2536
2836
|
)
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
)
|
|
2544
|
-
package_returncode = _run_checked(
|
|
2545
|
-
_bash_command(root, _PACKAGE_COMMAND, *package_arguments),
|
|
2546
|
-
env=child_env,
|
|
2837
|
+
local_install = _install_application_from_local_seed(
|
|
2838
|
+
root,
|
|
2839
|
+
final_app=final_app,
|
|
2840
|
+
final_posix=final_posix,
|
|
2841
|
+
version=version,
|
|
2842
|
+
environ=environ,
|
|
2547
2843
|
reporter=reporter,
|
|
2548
2844
|
runner=runner,
|
|
2549
|
-
|
|
2550
|
-
allow_failure=True,
|
|
2845
|
+
probe=probe,
|
|
2551
2846
|
)
|
|
2847
|
+
if local_install == "failed":
|
|
2848
|
+
try:
|
|
2849
|
+
shutil.rmtree(final_app / "venv")
|
|
2850
|
+
except OSError as exc:
|
|
2851
|
+
raise RuntimeInstallError(
|
|
2852
|
+
"could not reset the unpublished local application seed"
|
|
2853
|
+
) from exc
|
|
2854
|
+
_run_checked(
|
|
2855
|
+
_bash_command(root, _VENV_COMMAND, final_posix),
|
|
2856
|
+
env=child_env,
|
|
2857
|
+
reporter=reporter,
|
|
2858
|
+
runner=runner,
|
|
2859
|
+
label="Railmux virtual environment recovery",
|
|
2860
|
+
)
|
|
2861
|
+
if local_install == "ready":
|
|
2862
|
+
package_returncode = 0
|
|
2863
|
+
else:
|
|
2864
|
+
package_arguments = (
|
|
2865
|
+
final_posix,
|
|
2866
|
+
version,
|
|
2867
|
+
str(cache),
|
|
2868
|
+
str(_PIP_INITIAL_TIMEOUT_SECONDS),
|
|
2869
|
+
str(_PIP_INITIAL_RETRIES),
|
|
2870
|
+
)
|
|
2871
|
+
package_returncode = _run_checked(
|
|
2872
|
+
_bash_command(root, _PACKAGE_COMMAND, *package_arguments),
|
|
2873
|
+
env=child_env,
|
|
2874
|
+
reporter=reporter,
|
|
2875
|
+
runner=runner,
|
|
2876
|
+
label="Railmux runtime package installation",
|
|
2877
|
+
allow_failure=True,
|
|
2878
|
+
)
|
|
2552
2879
|
if package_returncode:
|
|
2553
2880
|
network_failure = (
|
|
2554
2881
|
reporter.command_had_network_failure
|
|
@@ -1256,6 +1256,7 @@ def test_preview_reports_info_status():
|
|
|
1256
1256
|
app._show_transcript = MagicMock(return_value=True)
|
|
1257
1257
|
app._set_active_target = MagicMock()
|
|
1258
1258
|
app._set_status = MagicMock()
|
|
1259
|
+
app._sessions_pane = MagicMock()
|
|
1259
1260
|
|
|
1260
1261
|
session = MagicMock()
|
|
1261
1262
|
session.display_title = "old chat"
|
|
@@ -1267,6 +1268,7 @@ def test_preview_reports_info_status():
|
|
|
1267
1268
|
assert app._in_history_mode is True
|
|
1268
1269
|
msg = app._set_status.call_args.args[0]
|
|
1269
1270
|
assert "Previewing" in msg and "old chat" in msg
|
|
1271
|
+
app._sessions_pane.set_selected_session.assert_not_called()
|
|
1270
1272
|
|
|
1271
1273
|
|
|
1272
1274
|
def test_preview_targets_explicit_active_secondary_slot():
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from unittest.mock import MagicMock
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
7
8
|
import urwid
|
|
@@ -273,6 +274,38 @@ def test_stale_running_registry_does_not_hide_stopped_preview(monkeypatch):
|
|
|
273
274
|
assert previewed == [True]
|
|
274
275
|
|
|
275
276
|
|
|
277
|
+
def test_session_row_click_paints_before_synchronous_preview(monkeypatch):
|
|
278
|
+
"""Windows tmux work must not postpone the pointer acknowledgement."""
|
|
279
|
+
from railmux.ui.app import App
|
|
280
|
+
|
|
281
|
+
project = _project()
|
|
282
|
+
session = _session(project)
|
|
283
|
+
app = App.__new__(App)
|
|
284
|
+
events = []
|
|
285
|
+
app._sessions_pane = MagicMock()
|
|
286
|
+
app._sessions_pane.set_selected_session.side_effect = (
|
|
287
|
+
lambda value: events.append(("selected", value))
|
|
288
|
+
)
|
|
289
|
+
app._loop = MagicMock()
|
|
290
|
+
app._loop.draw_screen.side_effect = lambda: events.append(("draw", None))
|
|
291
|
+
monkeypatch.setattr(app, "_by_session_id", lambda _session_id: None)
|
|
292
|
+
monkeypatch.setattr(
|
|
293
|
+
app,
|
|
294
|
+
"_on_session_preview",
|
|
295
|
+
lambda _session: events.append(("preview", _session.session_id)),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
app._on_session_row_preview(session)
|
|
299
|
+
|
|
300
|
+
assert events == [
|
|
301
|
+
("selected", session.session_id),
|
|
302
|
+
("draw", None),
|
|
303
|
+
("preview", session.session_id),
|
|
304
|
+
("selected", None),
|
|
305
|
+
("draw", None),
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
|
|
276
309
|
def test_agent_session_liveness_uses_displayed_swap_pane(monkeypatch):
|
|
277
310
|
"""A swap placeholder session must not mask death of the real pane."""
|
|
278
311
|
from types import SimpleNamespace
|
|
@@ -72,9 +72,10 @@ def test_noninteractive_missing_runtime_is_actionable_and_never_installs(capsys)
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def test_runtime_status_reports_missing_without_installing(capsys):
|
|
75
|
+
environ = {"LOCALAPPDATA": r"C:\Users\u\AppData\Local"}
|
|
75
76
|
result = main(
|
|
76
77
|
["runtime", "status"],
|
|
77
|
-
environ=
|
|
78
|
+
environ=environ,
|
|
78
79
|
runtime_finder=lambda **_kwargs: None,
|
|
79
80
|
version_info=(3, 10),
|
|
80
81
|
)
|
|
@@ -83,6 +84,34 @@ def test_runtime_status_reports_missing_without_installing(capsys):
|
|
|
83
84
|
output = capsys.readouterr().out
|
|
84
85
|
assert "Status: not installed" in output
|
|
85
86
|
assert "Provider data: shared" in output
|
|
87
|
+
data_root = windows_bootstrap.managed_windows_data_root(environ)
|
|
88
|
+
assert f"Data root: {data_root}" in output
|
|
89
|
+
assert f"Install logs: {data_root / 'logs'}" in output
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_runtime_status_json_exposes_the_actual_packaged_python_log_root(
|
|
93
|
+
capsys, tmp_path, monkeypatch
|
|
94
|
+
):
|
|
95
|
+
data_root = tmp_path / ".railmux" / "windows"
|
|
96
|
+
monkeypatch.setattr(
|
|
97
|
+
windows_bootstrap,
|
|
98
|
+
"managed_windows_data_root",
|
|
99
|
+
lambda _environ: data_root,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
assert main(
|
|
103
|
+
["runtime", "status", "--json"],
|
|
104
|
+
environ={
|
|
105
|
+
"LOCALAPPDATA": str(tmp_path / "AppData" / "Local"),
|
|
106
|
+
"USERPROFILE": str(tmp_path),
|
|
107
|
+
},
|
|
108
|
+
runtime_finder=lambda **_kwargs: None,
|
|
109
|
+
version_info=(3, 10),
|
|
110
|
+
) == 1
|
|
111
|
+
|
|
112
|
+
snapshot = json.loads(capsys.readouterr().out)
|
|
113
|
+
assert snapshot["data_root"] == str(data_root)
|
|
114
|
+
assert snapshot["log_directory"] == str(data_root / "logs")
|
|
86
115
|
|
|
87
116
|
|
|
88
117
|
def test_runtime_status_labels_user_owned_override(capsys, tmp_path):
|
|
@@ -64,6 +64,18 @@ def test_verbose_reporter_streams_the_same_sanitized_output(tmp_path):
|
|
|
64
64
|
assert "package 中文\nprogress\n" in path.read_text(encoding="utf-8")
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def test_install_log_has_utf8_signature_for_windows_powershell(tmp_path):
|
|
68
|
+
path = tmp_path / "install-0.4.0.dev32-20260806T000000Z-2.log"
|
|
69
|
+
|
|
70
|
+
with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
|
|
71
|
+
reporter.done("共享基础 · 正常")
|
|
72
|
+
|
|
73
|
+
assert path.read_bytes().startswith(b"\xef\xbb\xbf")
|
|
74
|
+
rendered = path.read_text(encoding="utf-8-sig")
|
|
75
|
+
assert rendered.startswith("Railmux Windows runtime installation")
|
|
76
|
+
assert "共享基础 · 正常" in rendered
|
|
77
|
+
|
|
78
|
+
|
|
67
79
|
def test_reporter_recognizes_only_the_explicit_msys2_core_restart(tmp_path):
|
|
68
80
|
path = tmp_path / "install.log"
|
|
69
81
|
with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
|