railmux 0.4.0.dev19__tar.gz → 0.4.0.dev21__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.dev19/src/railmux.egg-info → railmux-0.4.0.dev21}/PKG-INFO +8 -6
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/README.md +7 -5
- railmux-0.4.0.dev21/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/cli.py +15 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/fast_display_client.py +114 -25
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/fast_display_history.py +134 -82
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/fast_display_server.py +33 -16
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/tmux_ctl.py +7 -41
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/tmux_server.py +21 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/app.py +244 -42
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/modals.py +128 -25
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21/src/railmux.egg-info}/PKG-INFO +8 -6
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_agent_workspace.py +1 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_cli.py +26 -0
- railmux-0.4.0.dev21/tests/test_codex_lineage.py +561 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_fast_full_window.py +339 -79
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_focus_highlight.py +6 -2
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_modals.py +88 -8
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_soft_quit.py +101 -1
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tmux_ctl.py +0 -24
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tmux_integration.py +12 -49
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tmux_server.py +40 -4
- railmux-0.4.0.dev19/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev19/tests/test_codex_lineage.py +0 -253
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/LICENSE +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/pyproject.toml +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/setup.cfg +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev19 → railmux-0.4.0.dev21}/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.dev21
|
|
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
|
|
@@ -384,10 +384,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
|
384
384
|
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
385
385
|
per-pane history and normally preserves the live terminal's captured styling.
|
|
386
386
|
When Railmux confirms that a running Codex session advanced after rewind or
|
|
387
|
-
steering, it
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
rollout so its abandoned suffix stays hidden.
|
|
387
|
+
steering, it leaves the live terminal untouched and advances only that pane's
|
|
388
|
+
SSH history generation. The SSH history for that branched generation uses the
|
|
389
|
+
canonical rollout so its abandoned suffix stays hidden.
|
|
391
390
|
|
|
392
391
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
393
392
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -446,7 +445,10 @@ Each opened agent runs in a detached tmux session, so switching sessions does
|
|
|
446
445
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
447
446
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
448
447
|
restores the usable workspace when those sessions are still available. A normal
|
|
449
|
-
quit
|
|
448
|
+
quit ends all running sessions instead and therefore requires a second
|
|
449
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
450
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
451
|
+
existing confirm meaning in both steps.
|
|
450
452
|
|
|
451
453
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
452
454
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -341,10 +341,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
|
341
341
|
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
342
342
|
per-pane history and normally preserves the live terminal's captured styling.
|
|
343
343
|
When Railmux confirms that a running Codex session advanced after rewind or
|
|
344
|
-
steering, it
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
rollout so its abandoned suffix stays hidden.
|
|
344
|
+
steering, it leaves the live terminal untouched and advances only that pane's
|
|
345
|
+
SSH history generation. The SSH history for that branched generation uses the
|
|
346
|
+
canonical rollout so its abandoned suffix stays hidden.
|
|
348
347
|
|
|
349
348
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
350
349
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -403,7 +402,10 @@ Each opened agent runs in a detached tmux session, so switching sessions does
|
|
|
403
402
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
404
403
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
405
404
|
restores the usable workspace when those sessions are still available. A normal
|
|
406
|
-
quit
|
|
405
|
+
quit ends all running sessions instead and therefore requires a second
|
|
406
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
407
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
408
|
+
existing confirm meaning in both steps.
|
|
407
409
|
|
|
408
410
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
409
411
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev21"
|
|
@@ -45,6 +45,20 @@ _LOCAL_WATCHDOG_INTERVAL = 5.0
|
|
|
45
45
|
_LOCAL_WATCHDOG_FAILURES = 3
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _interactive_terminal_size() -> tuple[int, int] | None:
|
|
49
|
+
"""Return an exact entry TTY size without manufacturing a fallback."""
|
|
50
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
51
|
+
return None
|
|
52
|
+
for stream in (sys.stdout, sys.stdin):
|
|
53
|
+
try:
|
|
54
|
+
size = os.get_terminal_size(stream.fileno())
|
|
55
|
+
except (AttributeError, OSError):
|
|
56
|
+
continue
|
|
57
|
+
if 0 < size.columns <= 65535 and 0 < size.lines <= 65535:
|
|
58
|
+
return size.columns, size.lines
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
48
62
|
def _restore_terminal(attributes: list | None) -> None:
|
|
49
63
|
if attributes is None:
|
|
50
64
|
return
|
|
@@ -627,6 +641,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
627
641
|
launch_prefix,
|
|
628
642
|
raw_args,
|
|
629
643
|
env=client_env,
|
|
644
|
+
initial_size=_interactive_terminal_size(),
|
|
630
645
|
)
|
|
631
646
|
)
|
|
632
647
|
cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
|
|
@@ -39,7 +39,7 @@ from railmux.fast_display_history import (
|
|
|
39
39
|
HistoryAction,
|
|
40
40
|
LocalHistoryView,
|
|
41
41
|
PeriodicPrefetchGate,
|
|
42
|
-
|
|
42
|
+
claim_batched_forwarded_wheel,
|
|
43
43
|
input_may_change_routes,
|
|
44
44
|
)
|
|
45
45
|
from railmux.fast_display_input import (
|
|
@@ -1423,6 +1423,73 @@ class TerminalSurface:
|
|
|
1423
1423
|
self.interaction_active = False
|
|
1424
1424
|
|
|
1425
1425
|
|
|
1426
|
+
@dataclass
|
|
1427
|
+
class _DeferredHistoryPaint:
|
|
1428
|
+
"""Collapse one local-input batch into its final history viewport paint."""
|
|
1429
|
+
|
|
1430
|
+
render_history: bool = False
|
|
1431
|
+
restore_live: bool = False
|
|
1432
|
+
|
|
1433
|
+
def defer(self, action: HistoryAction) -> None:
|
|
1434
|
+
self.render_history = self.render_history or action.render_history
|
|
1435
|
+
self.restore_live = self.restore_live or action.restore_live
|
|
1436
|
+
|
|
1437
|
+
def flush(
|
|
1438
|
+
self,
|
|
1439
|
+
surface: TerminalSurface,
|
|
1440
|
+
screen: AppliedScreen | None,
|
|
1441
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1442
|
+
selection: tuple[SelectionSegment, ...],
|
|
1443
|
+
) -> None:
|
|
1444
|
+
if screen is not None:
|
|
1445
|
+
if self.restore_live:
|
|
1446
|
+
# A viewport may have reached the live bottom and another may
|
|
1447
|
+
# still be frozen. Restore the authoritative frame and compose
|
|
1448
|
+
# only the final overlays in one terminal write.
|
|
1449
|
+
surface.paint(full_repaint(screen), overlays, selection)
|
|
1450
|
+
elif self.render_history:
|
|
1451
|
+
surface.paint_overlays(screen, overlays, selection)
|
|
1452
|
+
self.render_history = False
|
|
1453
|
+
self.restore_live = False
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
class _LocalInputBatch:
|
|
1457
|
+
"""Preserve local wheel distance while bounding forwarded wheel bursts."""
|
|
1458
|
+
|
|
1459
|
+
def __init__(self) -> None:
|
|
1460
|
+
self._forwarded_wheel_directions: set[int] = set()
|
|
1461
|
+
self._history_paint = _DeferredHistoryPaint()
|
|
1462
|
+
|
|
1463
|
+
def prepare(
|
|
1464
|
+
self,
|
|
1465
|
+
event: SgrMouseEvent,
|
|
1466
|
+
action: HistoryAction,
|
|
1467
|
+
) -> tuple[HistoryAction | None, _DeferredHistoryPaint | None]:
|
|
1468
|
+
if event.wheel_direction == 0:
|
|
1469
|
+
return action, None
|
|
1470
|
+
if action.forwarded_input:
|
|
1471
|
+
if not claim_batched_forwarded_wheel(
|
|
1472
|
+
event,
|
|
1473
|
+
self._forwarded_wheel_directions,
|
|
1474
|
+
):
|
|
1475
|
+
return None, None
|
|
1476
|
+
return action, None
|
|
1477
|
+
# Every locally owned history tick has already advanced the viewport.
|
|
1478
|
+
# Defer only its terminal paint so a Windows/RDP read containing
|
|
1479
|
+
# several SGR packets preserves the complete distance without issuing
|
|
1480
|
+
# one synchronous pane repaint per packet.
|
|
1481
|
+
return action, self._history_paint
|
|
1482
|
+
|
|
1483
|
+
def flush(
|
|
1484
|
+
self,
|
|
1485
|
+
surface: TerminalSurface,
|
|
1486
|
+
screen: AppliedScreen | None,
|
|
1487
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1488
|
+
selection: tuple[SelectionSegment, ...],
|
|
1489
|
+
) -> None:
|
|
1490
|
+
self._history_paint.flush(surface, screen, overlays, selection)
|
|
1491
|
+
|
|
1492
|
+
|
|
1426
1493
|
def _remote_server_args(
|
|
1427
1494
|
*,
|
|
1428
1495
|
session: str,
|
|
@@ -2605,22 +2672,28 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2605
2672
|
if frame is not None:
|
|
2606
2673
|
send_protocol_frame(frame)
|
|
2607
2674
|
|
|
2608
|
-
def apply_history_action(
|
|
2675
|
+
def apply_history_action(
|
|
2676
|
+
action: HistoryAction,
|
|
2677
|
+
deferred_paint: _DeferredHistoryPaint | None = None,
|
|
2678
|
+
) -> None:
|
|
2609
2679
|
nonlocal route_refresh_needed
|
|
2610
2680
|
nonlocal history_info_until, claude_history_prompt_input
|
|
2611
2681
|
overlays = history.overlays()
|
|
2612
|
-
if
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2682
|
+
if deferred_paint is not None:
|
|
2683
|
+
deferred_paint.defer(action)
|
|
2684
|
+
else:
|
|
2685
|
+
if action.restore_live and latest_screen is not None:
|
|
2686
|
+
surface.paint(
|
|
2687
|
+
full_repaint(latest_screen),
|
|
2688
|
+
overlays,
|
|
2689
|
+
selection.segments(),
|
|
2690
|
+
)
|
|
2691
|
+
elif action.render_history and latest_screen is not None:
|
|
2692
|
+
surface.paint_overlays(
|
|
2693
|
+
latest_screen,
|
|
2694
|
+
overlays,
|
|
2695
|
+
selection.segments(),
|
|
2696
|
+
)
|
|
2624
2697
|
if action.protocol_frame:
|
|
2625
2698
|
send_protocol_frame(action.protocol_frame)
|
|
2626
2699
|
if action.forwarded_input:
|
|
@@ -2637,7 +2710,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2637
2710
|
|
|
2638
2711
|
def handle_terminal_part(
|
|
2639
2712
|
part: bytes | SgrMouseEvent,
|
|
2640
|
-
|
|
2713
|
+
input_batch: _LocalInputBatch,
|
|
2641
2714
|
) -> None:
|
|
2642
2715
|
nonlocal route_refresh_needed
|
|
2643
2716
|
nonlocal claude_history_prompt_input, claude_history_pending_choice
|
|
@@ -2900,15 +2973,18 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2900
2973
|
selection_changed_rows(selection_before, selection_after),
|
|
2901
2974
|
)
|
|
2902
2975
|
for replay_event in selection_action.replay_events:
|
|
2903
|
-
if not claim_batched_wheel(replay_event, handled_wheels):
|
|
2904
|
-
continue
|
|
2905
2976
|
replay_action = history.pointer_event(
|
|
2906
2977
|
replay_event,
|
|
2907
2978
|
focused_pane_id,
|
|
2908
2979
|
status_row=status_row,
|
|
2909
2980
|
now=time.monotonic(),
|
|
2910
2981
|
)
|
|
2911
|
-
|
|
2982
|
+
replay_action, deferred_paint = input_batch.prepare(
|
|
2983
|
+
replay_event,
|
|
2984
|
+
replay_action,
|
|
2985
|
+
)
|
|
2986
|
+
if replay_action is not None:
|
|
2987
|
+
apply_history_action(replay_action, deferred_paint)
|
|
2912
2988
|
if selection_action.copy_data is not None:
|
|
2913
2989
|
surface.copy_to_clipboard(selection_action.copy_data)
|
|
2914
2990
|
character_count = len(
|
|
@@ -2944,15 +3020,15 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2944
3020
|
begin_path_open(target)
|
|
2945
3021
|
if selection_action.handled:
|
|
2946
3022
|
return
|
|
2947
|
-
if not claim_batched_wheel(part, handled_wheels):
|
|
2948
|
-
return
|
|
2949
3023
|
action = history.pointer_event(
|
|
2950
3024
|
part,
|
|
2951
3025
|
focused_pane_id,
|
|
2952
3026
|
status_row=status_row,
|
|
2953
3027
|
now=time.monotonic(),
|
|
2954
3028
|
)
|
|
2955
|
-
|
|
3029
|
+
action, deferred_paint = input_batch.prepare(part, action)
|
|
3030
|
+
if action is not None:
|
|
3031
|
+
apply_history_action(action, deferred_paint)
|
|
2956
3032
|
return
|
|
2957
3033
|
if not part:
|
|
2958
3034
|
return
|
|
@@ -3327,19 +3403,32 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3327
3403
|
data, emergency_exit = split_local_escape(data)
|
|
3328
3404
|
if emergency_exit:
|
|
3329
3405
|
local_exit = True
|
|
3330
|
-
|
|
3406
|
+
input_batch = _LocalInputBatch()
|
|
3331
3407
|
for part in terminal_input.feed(data):
|
|
3332
3408
|
if isinstance(part, bytes):
|
|
3333
3409
|
for key_part in split_page_key_input(part):
|
|
3334
|
-
handle_terminal_part(key_part,
|
|
3410
|
+
handle_terminal_part(key_part, input_batch)
|
|
3335
3411
|
else:
|
|
3336
|
-
handle_terminal_part(part,
|
|
3412
|
+
handle_terminal_part(part, input_batch)
|
|
3413
|
+
input_batch.flush(
|
|
3414
|
+
surface,
|
|
3415
|
+
latest_screen,
|
|
3416
|
+
history.overlays(),
|
|
3417
|
+
selection.segments(),
|
|
3418
|
+
)
|
|
3337
3419
|
if local_exit:
|
|
3338
3420
|
break
|
|
3339
3421
|
if not local_exit:
|
|
3340
3422
|
for part in terminal_input.flush_pending():
|
|
3423
|
+
input_batch = _LocalInputBatch()
|
|
3341
3424
|
for key_part in split_page_key_input(part):
|
|
3342
|
-
handle_terminal_part(key_part,
|
|
3425
|
+
handle_terminal_part(key_part, input_batch)
|
|
3426
|
+
input_batch.flush(
|
|
3427
|
+
surface,
|
|
3428
|
+
latest_screen,
|
|
3429
|
+
history.overlays(),
|
|
3430
|
+
selection.segments(),
|
|
3431
|
+
)
|
|
3343
3432
|
now = time.monotonic()
|
|
3344
3433
|
keyboard_projected = touch_keyboard.keyboard_projected
|
|
3345
3434
|
if touch_keyboard.expire(now):
|
|
@@ -109,6 +109,10 @@ class _PendingHistory:
|
|
|
109
109
|
pane_id: str
|
|
110
110
|
target_lines: int
|
|
111
111
|
requested_at: float
|
|
112
|
+
# A first wheel-up waits for one cumulative snapshot before freezing the
|
|
113
|
+
# viewport. This avoids exposing a short hot suffix whose later deep page
|
|
114
|
+
# may not share a stable anchor while the provider is actively repainting.
|
|
115
|
+
initial_offset: int | None = None
|
|
112
116
|
|
|
113
117
|
|
|
114
118
|
class LocalHistoryView:
|
|
@@ -266,8 +270,6 @@ class LocalHistoryView:
|
|
|
266
270
|
self,
|
|
267
271
|
previous: HistorySnapshot,
|
|
268
272
|
incoming: HistorySnapshot,
|
|
269
|
-
*,
|
|
270
|
-
prefer_deeper: bool = False,
|
|
271
273
|
) -> HistorySnapshot:
|
|
272
274
|
"""Retain already-fetched history when a later hot capture shrinks."""
|
|
273
275
|
if not self._same_geometry(
|
|
@@ -276,28 +278,13 @@ class LocalHistoryView:
|
|
|
276
278
|
return incoming
|
|
277
279
|
delta = self._timeline_delta(previous.lines, incoming.lines)
|
|
278
280
|
if delta is None:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# cache; keeping only its live tail would silently discard all
|
|
287
|
-
# intermediate output above the current screen.
|
|
288
|
-
lines = incoming.lines
|
|
289
|
-
else:
|
|
290
|
-
# Never splice two captures whose timelines cannot be aligned.
|
|
291
|
-
# Retaining old history and replacing only the live viewport
|
|
292
|
-
# looked conservative, but it silently omitted every row
|
|
293
|
-
# produced between those two pieces. That false seam appeared
|
|
294
|
-
# as discontinuous SSH history until returning to the bottom
|
|
295
|
-
# forced a fresh deep capture. Keep the newest internally
|
|
296
|
-
# contiguous hot capture instead; the first wheel-up requests
|
|
297
|
-
# a cumulative deep page and can recover older rows safely.
|
|
298
|
-
# An already-frozen viewport owns its immutable snapshot and
|
|
299
|
-
# therefore remains undisturbed by this cache replacement.
|
|
300
|
-
lines = incoming.lines
|
|
281
|
+
# Never splice two captures whose timelines cannot be aligned.
|
|
282
|
+
# Retaining old history and replacing only the live viewport
|
|
283
|
+
# looked conservative, but it silently omitted every row produced
|
|
284
|
+
# between those two pieces. Keep the newest internally contiguous
|
|
285
|
+
# capture instead. An already-frozen viewport owns its immutable
|
|
286
|
+
# snapshot and therefore remains undisturbed by cache replacement.
|
|
287
|
+
lines = incoming.lines
|
|
301
288
|
else:
|
|
302
289
|
start = min(0, delta)
|
|
303
290
|
incoming_end = delta + len(incoming.lines)
|
|
@@ -325,8 +312,6 @@ class LocalHistoryView:
|
|
|
325
312
|
def _remember_content(
|
|
326
313
|
self,
|
|
327
314
|
snapshot: HistorySnapshot,
|
|
328
|
-
*,
|
|
329
|
-
prefer_deeper: bool = False,
|
|
330
315
|
) -> HistorySnapshot:
|
|
331
316
|
assert snapshot.pane_id is not None
|
|
332
317
|
previous = self.content_cache.get(snapshot.pane_id)
|
|
@@ -344,8 +329,7 @@ class LocalHistoryView:
|
|
|
344
329
|
stored = self._merge_content(previous, snapshot) if anchored else snapshot
|
|
345
330
|
self._unverified_after_reconnect.discard(snapshot.pane_id)
|
|
346
331
|
else:
|
|
347
|
-
stored = self._merge_content(
|
|
348
|
-
previous, snapshot, prefer_deeper=prefer_deeper)
|
|
332
|
+
stored = self._merge_content(previous, snapshot)
|
|
349
333
|
# Reinsert an existing pane to keep insertion order as recency order.
|
|
350
334
|
self.content_cache.pop(snapshot.pane_id, None)
|
|
351
335
|
self.content_cache[snapshot.pane_id] = stored
|
|
@@ -353,6 +337,15 @@ class LocalHistoryView:
|
|
|
353
337
|
del self.content_cache[next(iter(self.content_cache))]
|
|
354
338
|
return stored
|
|
355
339
|
|
|
340
|
+
def _replace_content(self, snapshot: HistorySnapshot) -> HistorySnapshot:
|
|
341
|
+
"""Install one verified contiguous capture without timeline merging."""
|
|
342
|
+
assert snapshot.pane_id is not None
|
|
343
|
+
self.content_cache.pop(snapshot.pane_id, None)
|
|
344
|
+
self.content_cache[snapshot.pane_id] = snapshot
|
|
345
|
+
while len(self.content_cache) > _HISTORY_CONTENT_PANES:
|
|
346
|
+
del self.content_cache[next(iter(self.content_cache))]
|
|
347
|
+
return snapshot
|
|
348
|
+
|
|
356
349
|
def invalidate_routes(self) -> bool:
|
|
357
350
|
"""Drop pointer authority without discarding bounded pane content."""
|
|
358
351
|
was_active = self.cancel()
|
|
@@ -470,29 +463,32 @@ class LocalHistoryView:
|
|
|
470
463
|
cached = self.content_cache.get(route.pane_id, route)
|
|
471
464
|
if not self._same_geometry(cached, route):
|
|
472
465
|
cached = route
|
|
466
|
+
loaded_limit = min(len(cached.lines), self.history_limit)
|
|
467
|
+
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
468
|
+
if (
|
|
469
|
+
cached.more_available
|
|
470
|
+
and loaded_limit < min(target_lines, _HISTORY_PREFETCH_LINES)
|
|
471
|
+
):
|
|
472
|
+
self.cancel_pane(route.pane_id)
|
|
473
|
+
request_id = self._allocate_request_id()
|
|
474
|
+
self._deep_pending[request_id] = _PendingHistory(
|
|
475
|
+
self.route_epoch,
|
|
476
|
+
route.pane_id,
|
|
477
|
+
target_lines,
|
|
478
|
+
time.monotonic() if now is None else now,
|
|
479
|
+
max(1, initial_offset),
|
|
480
|
+
)
|
|
481
|
+
self._deep_requests += 1
|
|
482
|
+
# A byte-budgeted or older peer may return less than the normal
|
|
483
|
+
# coherent routing capture. Keep the live pane intact until one
|
|
484
|
+
# cumulative response arrives rather than expose a short suffix.
|
|
485
|
+
return HistoryAction(
|
|
486
|
+
protocol_frame=encode_history_request(
|
|
487
|
+
request_id, event.x, event.y, target_lines
|
|
488
|
+
)
|
|
489
|
+
)
|
|
473
490
|
maximum = max(0, len(cached.lines) - cached.height)
|
|
474
491
|
if maximum == 0:
|
|
475
|
-
if cached.more_available:
|
|
476
|
-
self.cancel_pane(route.pane_id)
|
|
477
|
-
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
478
|
-
self.viewports[route.pane_id] = _HistoryViewport(
|
|
479
|
-
cached,
|
|
480
|
-
0,
|
|
481
|
-
min(len(cached.lines), self.history_limit),
|
|
482
|
-
)
|
|
483
|
-
request_id = self._allocate_request_id()
|
|
484
|
-
self._deep_pending[request_id] = _PendingHistory(
|
|
485
|
-
self.route_epoch,
|
|
486
|
-
route.pane_id,
|
|
487
|
-
target_lines,
|
|
488
|
-
time.monotonic() if now is None else now,
|
|
489
|
-
)
|
|
490
|
-
self._deep_requests += 1
|
|
491
|
-
return HistoryAction(
|
|
492
|
-
protocol_frame=encode_history_request(
|
|
493
|
-
request_id, event.x, event.y, target_lines
|
|
494
|
-
)
|
|
495
|
-
)
|
|
496
492
|
if cached.transcript_backed:
|
|
497
493
|
# Claude may not have written its first transcript record yet.
|
|
498
494
|
# This is a normal transient state, not a Railmux status event:
|
|
@@ -503,7 +499,6 @@ class LocalHistoryView:
|
|
|
503
499
|
forwarded_input=event.raw if cached.mouse_forwardable else b""
|
|
504
500
|
)
|
|
505
501
|
self.cancel_pane(route.pane_id)
|
|
506
|
-
loaded_limit = min(len(cached.lines), self.history_limit)
|
|
507
502
|
viewport = _HistoryViewport(
|
|
508
503
|
cached,
|
|
509
504
|
min(maximum, max(1, initial_offset)),
|
|
@@ -513,23 +508,15 @@ class LocalHistoryView:
|
|
|
513
508
|
),
|
|
514
509
|
)
|
|
515
510
|
self.viewports[route.pane_id] = viewport
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
render_history=True,
|
|
521
|
-
)
|
|
522
|
-
request_id = self._allocate_request_id()
|
|
523
|
-
self._deep_pending[request_id] = _PendingHistory(
|
|
524
|
-
self.route_epoch,
|
|
525
|
-
route.pane_id,
|
|
526
|
-
target_lines,
|
|
527
|
-
time.monotonic() if now is None else now,
|
|
528
|
-
)
|
|
529
|
-
self._deep_requests += 1
|
|
511
|
+
# A complete 300-row routing capture is already one coherent history
|
|
512
|
+
# source. Paint it synchronously, then fetch a cumulative page only as
|
|
513
|
+
# the viewport approaches its top. This removes an SSH round trip from
|
|
514
|
+
# the first wheel tick without reintroducing a hot/deep splice.
|
|
530
515
|
return HistoryAction(
|
|
531
|
-
protocol_frame=
|
|
532
|
-
|
|
516
|
+
protocol_frame=self._extend_history(
|
|
517
|
+
viewport,
|
|
518
|
+
now=now,
|
|
519
|
+
request_position=(event.x, event.y),
|
|
533
520
|
),
|
|
534
521
|
render_history=True,
|
|
535
522
|
)
|
|
@@ -539,6 +526,7 @@ class LocalHistoryView:
|
|
|
539
526
|
viewport: _HistoryViewport,
|
|
540
527
|
*,
|
|
541
528
|
now: float | None = None,
|
|
529
|
+
request_position: tuple[int, int] | None = None,
|
|
542
530
|
) -> bytes:
|
|
543
531
|
"""Request the next cumulative page when a viewport nears its top."""
|
|
544
532
|
snapshot = viewport.snapshot
|
|
@@ -572,7 +560,9 @@ class LocalHistoryView:
|
|
|
572
560
|
return b""
|
|
573
561
|
target_lines = min(
|
|
574
562
|
self.history_limit,
|
|
575
|
-
|
|
563
|
+
_HISTORY_INITIAL_LINES
|
|
564
|
+
if viewport.loaded_limit < _HISTORY_INITIAL_LINES
|
|
565
|
+
else viewport.loaded_limit + _HISTORY_PAGE_LINES,
|
|
576
566
|
)
|
|
577
567
|
request_id = self._allocate_request_id()
|
|
578
568
|
self._deep_pending[request_id] = _PendingHistory(
|
|
@@ -582,10 +572,14 @@ class LocalHistoryView:
|
|
|
582
572
|
requested_at,
|
|
583
573
|
)
|
|
584
574
|
self._deep_requests += 1
|
|
585
|
-
|
|
586
|
-
request_id,
|
|
575
|
+
request_x, request_y = request_position or (
|
|
587
576
|
snapshot.x + 1,
|
|
588
577
|
snapshot.y + 1,
|
|
578
|
+
)
|
|
579
|
+
return encode_history_request(
|
|
580
|
+
request_id,
|
|
581
|
+
request_x,
|
|
582
|
+
request_y,
|
|
589
583
|
target_lines,
|
|
590
584
|
)
|
|
591
585
|
|
|
@@ -602,6 +596,45 @@ class LocalHistoryView:
|
|
|
602
596
|
if direction > 0 and route.history_choice_required:
|
|
603
597
|
return HistoryAction(claude_history_prompt=event.raw)
|
|
604
598
|
viewport = self.viewports.get(route.pane_id)
|
|
599
|
+
initial_request = next(
|
|
600
|
+
(
|
|
601
|
+
(request_id, pending)
|
|
602
|
+
for request_id, pending in self._deep_pending.items()
|
|
603
|
+
if pending.pane_id == route.pane_id
|
|
604
|
+
and pending.initial_offset is not None
|
|
605
|
+
),
|
|
606
|
+
None,
|
|
607
|
+
)
|
|
608
|
+
if initial_request is not None:
|
|
609
|
+
request_id, pending = initial_request
|
|
610
|
+
requested_at = time.monotonic() if now is None else now
|
|
611
|
+
if requested_at - pending.requested_at >= _HISTORY_DEEP_TIMEOUT:
|
|
612
|
+
self._deep_pending.pop(request_id, None)
|
|
613
|
+
self._timeouts += 1
|
|
614
|
+
if direction < 0:
|
|
615
|
+
return HistoryAction()
|
|
616
|
+
assert pending.initial_offset is not None
|
|
617
|
+
distance = min(
|
|
618
|
+
self.history_limit,
|
|
619
|
+
pending.initial_offset + max(1, distance),
|
|
620
|
+
)
|
|
621
|
+
initial_request = None
|
|
622
|
+
if viewport is None and initial_request is not None:
|
|
623
|
+
if direction < 0:
|
|
624
|
+
# The live pane never left the screen, so reversing direction
|
|
625
|
+
# simply cancels the pending entry. A late response is ignored.
|
|
626
|
+
self.cancel_pane(route.pane_id)
|
|
627
|
+
return HistoryAction()
|
|
628
|
+
request_id, pending = initial_request
|
|
629
|
+
assert pending.initial_offset is not None
|
|
630
|
+
self._deep_pending[request_id] = replace(
|
|
631
|
+
pending,
|
|
632
|
+
initial_offset=min(
|
|
633
|
+
self.history_limit,
|
|
634
|
+
pending.initial_offset + max(1, distance),
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
return HistoryAction()
|
|
605
638
|
if viewport is not None:
|
|
606
639
|
maximum = max(0, len(viewport.snapshot.lines) - viewport.snapshot.height)
|
|
607
640
|
viewport.offset = max(
|
|
@@ -828,8 +861,26 @@ class LocalHistoryView:
|
|
|
828
861
|
return HistoryAction()
|
|
829
862
|
viewport = self.viewports.get(pending.pane_id)
|
|
830
863
|
if viewport is None:
|
|
831
|
-
|
|
832
|
-
|
|
864
|
+
if pending.initial_offset is None:
|
|
865
|
+
self._remember_content(snapshot)
|
|
866
|
+
return HistoryAction()
|
|
867
|
+
maximum = max(0, len(snapshot.lines) - snapshot.height)
|
|
868
|
+
if maximum == 0:
|
|
869
|
+
self._remember_content(snapshot)
|
|
870
|
+
return HistoryAction()
|
|
871
|
+
stored = self._replace_content(snapshot)
|
|
872
|
+
maximum = max(0, len(stored.lines) - stored.height)
|
|
873
|
+
viewport = _HistoryViewport(
|
|
874
|
+
stored,
|
|
875
|
+
min(maximum, pending.initial_offset),
|
|
876
|
+
min(self.history_limit, max(pending.target_lines, len(stored.lines))),
|
|
877
|
+
exhausted=not snapshot.more_available,
|
|
878
|
+
)
|
|
879
|
+
self.viewports[pending.pane_id] = viewport
|
|
880
|
+
return HistoryAction(
|
|
881
|
+
protocol_frame=self._extend_history(viewport),
|
|
882
|
+
render_history=True,
|
|
883
|
+
)
|
|
833
884
|
maximum = max(0, len(snapshot.lines) - snapshot.height)
|
|
834
885
|
if maximum == 0:
|
|
835
886
|
self._remember_content(snapshot)
|
|
@@ -843,7 +894,11 @@ class LocalHistoryView:
|
|
|
843
894
|
# snapshot instead of jumping to newer or unrelated text.
|
|
844
895
|
self._anchor_rejects += 1
|
|
845
896
|
return HistoryAction()
|
|
846
|
-
|
|
897
|
+
# The response is one cumulative capture and its exact visible anchor
|
|
898
|
+
# was just proved above. Replace the cached timeline as a whole instead
|
|
899
|
+
# of merging it with the older hot generation; this keeps every row's
|
|
900
|
+
# text and background styling from one server render.
|
|
901
|
+
stored = self._replace_content(snapshot)
|
|
847
902
|
stored_offset = self._aligned_offset(stored, anchor)
|
|
848
903
|
if stored_offset is None:
|
|
849
904
|
# The raw response was valid, so this can only be an ambiguous
|
|
@@ -892,7 +947,7 @@ class LocalHistoryView:
|
|
|
892
947
|
return matched_offset
|
|
893
948
|
|
|
894
949
|
# Agent status rows (for example a Codex spinner) can change between
|
|
895
|
-
# the
|
|
950
|
+
# the hot snapshot and a deep response. Requiring the
|
|
896
951
|
# entire visible viewport to remain byte-identical then strands the
|
|
897
952
|
# user at the hot-cache boundary. Align on a majority of stable,
|
|
898
953
|
# non-blank lines instead, but only when each evidence line occurs
|
|
@@ -966,18 +1021,15 @@ class LocalHistoryView:
|
|
|
966
1021
|
return was_active
|
|
967
1022
|
|
|
968
1023
|
|
|
969
|
-
def
|
|
1024
|
+
def claim_batched_forwarded_wheel(
|
|
970
1025
|
event: SgrMouseEvent,
|
|
971
1026
|
handled_directions: set[int],
|
|
972
1027
|
) -> bool:
|
|
973
|
-
"""Admit at most one wheel tick per direction
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
lines at once. Bounding each read preserves the established one-line
|
|
979
|
-
local scroll feel without using a timing heuristic; a later read remains
|
|
980
|
-
immediately eligible.
|
|
1028
|
+
"""Admit at most one forwarded wheel tick per direction in one read.
|
|
1029
|
+
|
|
1030
|
+
Remote sidebar/modal owners need one bounded event rather than a stale
|
|
1031
|
+
backlog. Locally owned agent history must not use this helper: it retains
|
|
1032
|
+
the complete reported distance and coalesces only its terminal paint.
|
|
981
1033
|
"""
|
|
982
1034
|
direction = event.wheel_direction
|
|
983
1035
|
if direction == 0:
|