railmux 0.3.5.dev1__tar.gz → 0.3.6.dev2__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.5.dev1/src/railmux.egg-info → railmux-0.3.6.dev2}/PKG-INFO +18 -6
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/README.md +17 -5
- railmux-0.3.6.dev2/src/railmux/__init__.py +1 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_history.py +104 -34
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/models.py +3 -0
- railmux-0.3.6.dev2/src/railmux/session_lease.py +554 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_binding_manager.py +5 -2
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_ctl.py +7 -41
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/app.py +702 -87
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/modals.py +81 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/running_pane.py +4 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sessions_pane.py +10 -3
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2/src/railmux.egg-info}/PKG-INFO +18 -6
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_agent_workspace.py +1 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_codex_delete.py +4 -0
- railmux-0.3.6.dev2/tests/test_codex_lineage.py +561 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_fast_full_window.py +216 -50
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_focus_highlight.py +10 -2
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_modals.py +68 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_orphan_recovery.py +40 -1
- railmux-0.3.6.dev2/tests/test_session_lease.py +491 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_sessions_pane.py +15 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_soft_quit.py +101 -1
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_size.py +12 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_binding_manager.py +25 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_ctl.py +0 -24
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_integration.py +12 -49
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_status.py +57 -8
- railmux-0.3.5.dev1/src/railmux/__init__.py +0 -1
- railmux-0.3.5.dev1/tests/test_codex_lineage.py +0 -253
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/LICENSE +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/MANIFEST.in +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/pyproject.toml +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/setup.cfg +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/__main__.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/background_index.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/cli.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/config.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/discovery.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/favorites.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/launcher.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/local_open.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/modes.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/renames.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/self_update.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/session_index.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/settings.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/transcript.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_background_index.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_cli.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_codex_index.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_config.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_config_cli.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_discovery.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_display_transport.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_help_agent.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_keymap.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_launcher.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_local_open.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_modes.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_new_project.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_options.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_path_codec.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_release_notes.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_remote_config.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_renames.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_restart_state.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_running_pane.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_running_sort.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_self_update.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_session_cache.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_session_index.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_settings.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_sidebar.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_status.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_statusbar.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_system_deps.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_transcript.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_widgets.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tools/release_notes.py +0 -0
- {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6.dev2
|
|
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
|
|
@@ -427,13 +426,26 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
|
|
|
427
426
|
Railmux, so existing conversations appear in the sidebar and resume the same
|
|
428
427
|
way. Non-resumable one-off runs such as `codex exec` are filtered out.
|
|
429
428
|
|
|
429
|
+
If the same Claude Code or Codex history root is shared across hosts, Railmux
|
|
430
|
+
uses a provider-session lease to prevent two machines from resuming the same
|
|
431
|
+
conversation concurrently. A remotely owned conversation remains in Sessions
|
|
432
|
+
as **running on HOST**, but is not presented as a locally attachable Running
|
|
433
|
+
entry; resume fails safely until that host's exact provider pane exits. The
|
|
434
|
+
lease follows detached agents across Soft Quit and covers every known Codex
|
|
435
|
+
rewind-lineage ID at resume; later rewinds retain a stable locked lineage
|
|
436
|
+
anchor. Delete also checks the shared lease immediately before
|
|
437
|
+
starting and refuses remotely owned history. The shared filesystem must provide
|
|
438
|
+
cross-host POSIX advisory-lock semantics; mounts configured with local-only or
|
|
439
|
+
disabled locking cannot provide this guarantee.
|
|
440
|
+
|
|
430
441
|
### Restarts and quitting
|
|
431
442
|
|
|
432
443
|
Each opened agent runs in a detached tmux session, so switching sessions does
|
|
433
444
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
434
445
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
435
446
|
restores the usable workspace when those sessions are still available. A normal
|
|
436
|
-
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.
|
|
437
449
|
|
|
438
450
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
439
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
|
|
@@ -385,13 +384,26 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
|
|
|
385
384
|
Railmux, so existing conversations appear in the sidebar and resume the same
|
|
386
385
|
way. Non-resumable one-off runs such as `codex exec` are filtered out.
|
|
387
386
|
|
|
387
|
+
If the same Claude Code or Codex history root is shared across hosts, Railmux
|
|
388
|
+
uses a provider-session lease to prevent two machines from resuming the same
|
|
389
|
+
conversation concurrently. A remotely owned conversation remains in Sessions
|
|
390
|
+
as **running on HOST**, but is not presented as a locally attachable Running
|
|
391
|
+
entry; resume fails safely until that host's exact provider pane exits. The
|
|
392
|
+
lease follows detached agents across Soft Quit and covers every known Codex
|
|
393
|
+
rewind-lineage ID at resume; later rewinds retain a stable locked lineage
|
|
394
|
+
anchor. Delete also checks the shared lease immediately before
|
|
395
|
+
starting and refuses remotely owned history. The shared filesystem must provide
|
|
396
|
+
cross-host POSIX advisory-lock semantics; mounts configured with local-only or
|
|
397
|
+
disabled locking cannot provide this guarantee.
|
|
398
|
+
|
|
388
399
|
### Restarts and quitting
|
|
389
400
|
|
|
390
401
|
Each opened agent runs in a detached tmux session, so switching sessions does
|
|
391
402
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
392
403
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
393
404
|
restores the usable workspace when those sessions are still available. A normal
|
|
394
|
-
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.
|
|
395
407
|
|
|
396
408
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
397
409
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.6.dev2"
|
|
@@ -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:
|
|
@@ -287,15 +291,17 @@ class LocalHistoryView:
|
|
|
287
291
|
# intermediate output above the current screen.
|
|
288
292
|
lines = incoming.lines
|
|
289
293
|
else:
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
294
|
+
# Never splice two captures whose timelines cannot be aligned.
|
|
295
|
+
# Retaining old history and replacing only the live viewport
|
|
296
|
+
# looked conservative, but it silently omitted every row
|
|
297
|
+
# produced between those two pieces. That false seam appeared
|
|
298
|
+
# as discontinuous SSH history until returning to the bottom
|
|
299
|
+
# forced a fresh deep capture. Keep the newest internally
|
|
300
|
+
# contiguous hot capture instead; the first wheel-up requests
|
|
301
|
+
# a cumulative deep page and can recover older rows safely.
|
|
302
|
+
# An already-frozen viewport owns its immutable snapshot and
|
|
303
|
+
# therefore remains undisturbed by this cache replacement.
|
|
304
|
+
lines = incoming.lines
|
|
299
305
|
else:
|
|
300
306
|
start = min(0, delta)
|
|
301
307
|
incoming_end = delta + len(incoming.lines)
|
|
@@ -351,6 +357,15 @@ class LocalHistoryView:
|
|
|
351
357
|
del self.content_cache[next(iter(self.content_cache))]
|
|
352
358
|
return stored
|
|
353
359
|
|
|
360
|
+
def _replace_content(self, snapshot: HistorySnapshot) -> HistorySnapshot:
|
|
361
|
+
"""Install one verified contiguous capture without timeline merging."""
|
|
362
|
+
assert snapshot.pane_id is not None
|
|
363
|
+
self.content_cache.pop(snapshot.pane_id, None)
|
|
364
|
+
self.content_cache[snapshot.pane_id] = snapshot
|
|
365
|
+
while len(self.content_cache) > _HISTORY_CONTENT_PANES:
|
|
366
|
+
del self.content_cache[next(iter(self.content_cache))]
|
|
367
|
+
return snapshot
|
|
368
|
+
|
|
354
369
|
def invalidate_routes(self) -> bool:
|
|
355
370
|
"""Drop pointer authority without discarding bounded pane content."""
|
|
356
371
|
was_active = self.cancel()
|
|
@@ -468,29 +483,29 @@ class LocalHistoryView:
|
|
|
468
483
|
cached = self.content_cache.get(route.pane_id, route)
|
|
469
484
|
if not self._same_geometry(cached, route):
|
|
470
485
|
cached = route
|
|
486
|
+
loaded_limit = min(len(cached.lines), self.history_limit)
|
|
487
|
+
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
488
|
+
if cached.more_available and loaded_limit < target_lines:
|
|
489
|
+
self.cancel_pane(route.pane_id)
|
|
490
|
+
request_id = self._allocate_request_id()
|
|
491
|
+
self._deep_pending[request_id] = _PendingHistory(
|
|
492
|
+
self.route_epoch,
|
|
493
|
+
route.pane_id,
|
|
494
|
+
target_lines,
|
|
495
|
+
time.monotonic() if now is None else now,
|
|
496
|
+
max(1, initial_offset),
|
|
497
|
+
)
|
|
498
|
+
self._deep_requests += 1
|
|
499
|
+
# Keep the live pane intact until one internally contiguous deep
|
|
500
|
+
# capture arrives. Entering history atomically from that response
|
|
501
|
+
# removes the hot/deep seam and needs no moving-live anchor.
|
|
502
|
+
return HistoryAction(
|
|
503
|
+
protocol_frame=encode_history_request(
|
|
504
|
+
request_id, event.x, event.y, target_lines
|
|
505
|
+
)
|
|
506
|
+
)
|
|
471
507
|
maximum = max(0, len(cached.lines) - cached.height)
|
|
472
508
|
if maximum == 0:
|
|
473
|
-
if cached.more_available:
|
|
474
|
-
self.cancel_pane(route.pane_id)
|
|
475
|
-
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
476
|
-
self.viewports[route.pane_id] = _HistoryViewport(
|
|
477
|
-
cached,
|
|
478
|
-
0,
|
|
479
|
-
min(len(cached.lines), self.history_limit),
|
|
480
|
-
)
|
|
481
|
-
request_id = self._allocate_request_id()
|
|
482
|
-
self._deep_pending[request_id] = _PendingHistory(
|
|
483
|
-
self.route_epoch,
|
|
484
|
-
route.pane_id,
|
|
485
|
-
target_lines,
|
|
486
|
-
time.monotonic() if now is None else now,
|
|
487
|
-
)
|
|
488
|
-
self._deep_requests += 1
|
|
489
|
-
return HistoryAction(
|
|
490
|
-
protocol_frame=encode_history_request(
|
|
491
|
-
request_id, event.x, event.y, target_lines
|
|
492
|
-
)
|
|
493
|
-
)
|
|
494
509
|
if cached.transcript_backed:
|
|
495
510
|
# Claude may not have written its first transcript record yet.
|
|
496
511
|
# This is a normal transient state, not a Railmux status event:
|
|
@@ -501,7 +516,6 @@ class LocalHistoryView:
|
|
|
501
516
|
forwarded_input=event.raw if cached.mouse_forwardable else b""
|
|
502
517
|
)
|
|
503
518
|
self.cancel_pane(route.pane_id)
|
|
504
|
-
loaded_limit = min(len(cached.lines), self.history_limit)
|
|
505
519
|
viewport = _HistoryViewport(
|
|
506
520
|
cached,
|
|
507
521
|
min(maximum, max(1, initial_offset)),
|
|
@@ -511,7 +525,6 @@ class LocalHistoryView:
|
|
|
511
525
|
),
|
|
512
526
|
)
|
|
513
527
|
self.viewports[route.pane_id] = viewport
|
|
514
|
-
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
515
528
|
if loaded_limit >= target_lines:
|
|
516
529
|
return HistoryAction(
|
|
517
530
|
protocol_frame=self._extend_history(viewport, now=now),
|
|
@@ -600,6 +613,45 @@ class LocalHistoryView:
|
|
|
600
613
|
if direction > 0 and route.history_choice_required:
|
|
601
614
|
return HistoryAction(claude_history_prompt=event.raw)
|
|
602
615
|
viewport = self.viewports.get(route.pane_id)
|
|
616
|
+
initial_request = next(
|
|
617
|
+
(
|
|
618
|
+
(request_id, pending)
|
|
619
|
+
for request_id, pending in self._deep_pending.items()
|
|
620
|
+
if pending.pane_id == route.pane_id
|
|
621
|
+
and pending.initial_offset is not None
|
|
622
|
+
),
|
|
623
|
+
None,
|
|
624
|
+
)
|
|
625
|
+
if initial_request is not None:
|
|
626
|
+
request_id, pending = initial_request
|
|
627
|
+
requested_at = time.monotonic() if now is None else now
|
|
628
|
+
if requested_at - pending.requested_at >= _HISTORY_DEEP_TIMEOUT:
|
|
629
|
+
self._deep_pending.pop(request_id, None)
|
|
630
|
+
self._timeouts += 1
|
|
631
|
+
if direction < 0:
|
|
632
|
+
return HistoryAction()
|
|
633
|
+
assert pending.initial_offset is not None
|
|
634
|
+
distance = min(
|
|
635
|
+
self.history_limit,
|
|
636
|
+
pending.initial_offset + max(1, distance),
|
|
637
|
+
)
|
|
638
|
+
initial_request = None
|
|
639
|
+
if viewport is None and initial_request is not None:
|
|
640
|
+
if direction < 0:
|
|
641
|
+
# The live pane never left the screen, so reversing direction
|
|
642
|
+
# simply cancels the pending entry. A late response is ignored.
|
|
643
|
+
self.cancel_pane(route.pane_id)
|
|
644
|
+
return HistoryAction()
|
|
645
|
+
request_id, pending = initial_request
|
|
646
|
+
assert pending.initial_offset is not None
|
|
647
|
+
self._deep_pending[request_id] = replace(
|
|
648
|
+
pending,
|
|
649
|
+
initial_offset=min(
|
|
650
|
+
self.history_limit,
|
|
651
|
+
pending.initial_offset + max(1, distance),
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
return HistoryAction()
|
|
603
655
|
if viewport is not None:
|
|
604
656
|
maximum = max(0, len(viewport.snapshot.lines) - viewport.snapshot.height)
|
|
605
657
|
viewport.offset = max(
|
|
@@ -826,8 +878,26 @@ class LocalHistoryView:
|
|
|
826
878
|
return HistoryAction()
|
|
827
879
|
viewport = self.viewports.get(pending.pane_id)
|
|
828
880
|
if viewport is None:
|
|
829
|
-
|
|
830
|
-
|
|
881
|
+
if pending.initial_offset is None:
|
|
882
|
+
self._remember_content(snapshot)
|
|
883
|
+
return HistoryAction()
|
|
884
|
+
maximum = max(0, len(snapshot.lines) - snapshot.height)
|
|
885
|
+
if maximum == 0:
|
|
886
|
+
self._remember_content(snapshot)
|
|
887
|
+
return HistoryAction()
|
|
888
|
+
stored = self._replace_content(snapshot)
|
|
889
|
+
maximum = max(0, len(stored.lines) - stored.height)
|
|
890
|
+
viewport = _HistoryViewport(
|
|
891
|
+
stored,
|
|
892
|
+
min(maximum, pending.initial_offset),
|
|
893
|
+
min(self.history_limit, max(pending.target_lines, len(stored.lines))),
|
|
894
|
+
exhausted=not snapshot.more_available,
|
|
895
|
+
)
|
|
896
|
+
self.viewports[pending.pane_id] = viewport
|
|
897
|
+
return HistoryAction(
|
|
898
|
+
protocol_frame=self._extend_history(viewport),
|
|
899
|
+
render_history=True,
|
|
900
|
+
)
|
|
831
901
|
maximum = max(0, len(snapshot.lines) - snapshot.height)
|
|
832
902
|
if maximum == 0:
|
|
833
903
|
self._remember_content(snapshot)
|
|
@@ -75,6 +75,9 @@ class SessionMeta:
|
|
|
75
75
|
# conversation without changing the provider-owned UUID used for resume.
|
|
76
76
|
# Always None for Claude sessions and older Codex rollouts.
|
|
77
77
|
forked_from_id: str | None = None
|
|
78
|
+
# A Railmux lease observed in the same shared provider history root. This
|
|
79
|
+
# is a read-time UI overlay, never provider transcript metadata.
|
|
80
|
+
remote_owner: str | None = None
|
|
78
81
|
|
|
79
82
|
@property
|
|
80
83
|
def display_title(self) -> str:
|