railmux 0.3.6.dev1__tar.gz → 0.3.6.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.3.6.dev1/src/railmux.egg-info → railmux-0.3.6.dev3}/PKG-INFO +6 -6
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/README.md +5 -5
- railmux-0.3.6.dev3/src/railmux/__init__.py +1 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_history.py +128 -73
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_binding_manager.py +5 -2
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_ctl.py +7 -41
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/app.py +244 -42
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/modals.py +81 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3/src/railmux.egg-info}/PKG-INFO +6 -6
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_agent_workspace.py +1 -0
- railmux-0.3.6.dev3/tests/test_codex_lineage.py +561 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_fast_full_window.py +210 -53
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_focus_highlight.py +6 -2
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_modals.py +68 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_soft_quit.py +101 -1
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_binding_manager.py +25 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_ctl.py +0 -24
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_integration.py +12 -49
- railmux-0.3.6.dev1/src/railmux/__init__.py +0 -1
- railmux-0.3.6.dev1/tests/test_codex_lineage.py +0 -253
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/LICENSE +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/MANIFEST.in +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/pyproject.toml +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/setup.cfg +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/__main__.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/background_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/discovery.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/favorites.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/launcher.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/local_open.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/models.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/modes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/renames.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/self_update.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/session_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/session_lease.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/settings.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/transcript.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_background_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_codex_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_config_cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_discovery.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_display_transport.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_help_agent.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_keymap.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_launcher.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_local_open.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_modes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_new_project.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_options.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_path_codec.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_release_notes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_remote_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_renames.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_restart_state.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_running_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_running_sort.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_self_update.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_session_cache.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_session_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_session_lease.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_settings.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_sidebar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_statusbar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_system_deps.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_transcript.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_widgets.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tools/release_notes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.6.
|
|
3
|
+
Version: 0.3.6.dev3
|
|
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
|
|
@@ -383,10 +383,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
|
383
383
|
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
384
384
|
per-pane history and normally preserves the live terminal's captured styling.
|
|
385
385
|
When Railmux confirms that a running Codex session advanced after rewind or
|
|
386
|
-
steering, it
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
rollout so its abandoned suffix stays hidden.
|
|
386
|
+
steering, it leaves the live terminal untouched and advances only that pane's
|
|
387
|
+
SSH history generation. The SSH history for that branched generation uses the
|
|
388
|
+
canonical rollout so its abandoned suffix stays hidden.
|
|
390
389
|
|
|
391
390
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
392
391
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -445,7 +444,8 @@ Each opened agent runs in a detached tmux session, so switching sessions does
|
|
|
445
444
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
446
445
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
447
446
|
restores the usable workspace when those sessions are still available. A normal
|
|
448
|
-
quit
|
|
447
|
+
quit ends all running sessions instead and therefore requires a second
|
|
448
|
+
confirmation; `y` and Enter keep their existing confirm meaning in both steps.
|
|
449
449
|
|
|
450
450
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
451
451
|
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,8 @@ 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; `y` and Enter keep their existing confirm meaning in both steps.
|
|
407
407
|
|
|
408
408
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
409
409
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.6.dev3"
|
|
@@ -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
|
|
@@ -502,7 +502,6 @@ class SharedTmuxBindingManager:
|
|
|
502
502
|
)
|
|
503
503
|
break
|
|
504
504
|
return False
|
|
505
|
-
self._registered = True
|
|
506
505
|
self._prefix_tab_managed = state["prefix_tab_managed"]
|
|
507
506
|
self._right_click_managed = (
|
|
508
507
|
state["right_click_managed"])
|
|
@@ -514,6 +513,10 @@ class SharedTmuxBindingManager:
|
|
|
514
513
|
state["status_click_managed"])
|
|
515
514
|
self._termux_tap_managed = (
|
|
516
515
|
state["termux_tap_managed"])
|
|
516
|
+
# Publish registration only after every capability is
|
|
517
|
+
# final so concurrent readers cannot observe a torn
|
|
518
|
+
# lease during managed-Windows background setup.
|
|
519
|
+
self._registered = True
|
|
517
520
|
return True
|
|
518
521
|
|
|
519
522
|
backup = tmux_ctl.prepare_root_function_bindings()
|
|
@@ -662,13 +665,13 @@ class SharedTmuxBindingManager:
|
|
|
662
665
|
state["termux_tap_backup"], token=token)
|
|
663
666
|
self._remove_state()
|
|
664
667
|
return False
|
|
665
|
-
self._registered = True
|
|
666
668
|
self._prefix_tab_managed = state["prefix_tab_managed"]
|
|
667
669
|
self._right_click_managed = state["right_click_managed"]
|
|
668
670
|
self._selection_hook_managed = state["selection_hook_managed"]
|
|
669
671
|
self._selection_hook_index = state["selection_hook_index"]
|
|
670
672
|
self._status_click_managed = state["status_click_managed"]
|
|
671
673
|
self._termux_tap_managed = state["termux_tap_managed"]
|
|
674
|
+
self._registered = True
|
|
672
675
|
return True
|
|
673
676
|
except OSError:
|
|
674
677
|
return False
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
|
-
import signal
|
|
12
11
|
import shlex
|
|
13
12
|
import shutil
|
|
14
13
|
import subprocess
|
|
@@ -1840,7 +1839,13 @@ RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
|
|
|
1840
1839
|
# A confirmed branch transition prefixes the opaque SSH history cache epoch.
|
|
1841
1840
|
# Plain generations use native pane history; only this explicit marker permits
|
|
1842
1841
|
# the canonical transcript fallback that removes an abandoned rewind suffix.
|
|
1843
|
-
|
|
1842
|
+
# Only the evidence-gated v2 marker authorizes transcript-backed Codex
|
|
1843
|
+
# scrolling. The released v1 marker treated every live direct child rollout
|
|
1844
|
+
# as a rewind, but Codex can also create that shape while bootstrapping an
|
|
1845
|
+
# ordinary resume. Keeping the prefix versioned makes existing over-broad
|
|
1846
|
+
# markers fail back to raw pane history on upgrade.
|
|
1847
|
+
RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v2:"
|
|
1848
|
+
RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX = "canonical:"
|
|
1844
1849
|
# tmux names function keys only through F12. Private restore routes use
|
|
1845
1850
|
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1846
1851
|
# user input forwarded to an agent pane.
|
|
@@ -3330,45 +3335,6 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
|
|
|
3330
3335
|
)
|
|
3331
3336
|
|
|
3332
3337
|
|
|
3333
|
-
def reset_pane_view(identity: PaneIdentity) -> bool:
|
|
3334
|
-
"""Reset visible terminal state without deleting retained scrollback.
|
|
3335
|
-
|
|
3336
|
-
Revalidate the immutable session, pane process, and window before issuing
|
|
3337
|
-
tmux's terminal reset. Then send the pane's foreground process group an
|
|
3338
|
-
ordinary same-size ``SIGWINCH`` so its TUI repaints and reasserts terminal
|
|
3339
|
-
modes without a geometry change or injected keyboard input. The reset can
|
|
3340
|
-
push the old viewport into tmux history, but never destroys the retained
|
|
3341
|
-
prefix; managed history surfaces use the provider's canonical transcript.
|
|
3342
|
-
"""
|
|
3343
|
-
current = pane_identity(identity.pane_id)
|
|
3344
|
-
if (current is None or current.dead
|
|
3345
|
-
or current.pane_pid != identity.pane_pid
|
|
3346
|
-
or current.session_id != identity.session_id
|
|
3347
|
-
or current.window_id != identity.window_id):
|
|
3348
|
-
return False
|
|
3349
|
-
try:
|
|
3350
|
-
subprocess.check_call(
|
|
3351
|
-
["tmux", "send-keys", "-R", "-t", identity.pane_id],
|
|
3352
|
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
3353
|
-
)
|
|
3354
|
-
except (OSError, subprocess.CalledProcessError):
|
|
3355
|
-
return False
|
|
3356
|
-
|
|
3357
|
-
current = pane_identity(identity.pane_id)
|
|
3358
|
-
if (current is None or current.dead
|
|
3359
|
-
or current.pane_pid != identity.pane_pid
|
|
3360
|
-
or current.session_id != identity.session_id
|
|
3361
|
-
or current.window_id != identity.window_id):
|
|
3362
|
-
return True
|
|
3363
|
-
try:
|
|
3364
|
-
os.killpg(os.getpgid(current.pane_pid), signal.SIGWINCH)
|
|
3365
|
-
except (OSError, ValueError):
|
|
3366
|
-
# The presentation cache is already clean. A provider that exited or
|
|
3367
|
-
# cannot be signalled will repaint on its next ordinary terminal write.
|
|
3368
|
-
pass
|
|
3369
|
-
return True
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
3338
|
def kill_session_identity(identity: PaneIdentity) -> bool:
|
|
3373
3339
|
"""Kill an exact immutable tmux session, never a reused session name."""
|
|
3374
3340
|
current = pane_identity(identity.pane_id)
|