railmux 0.4.2.dev2__tar.gz → 0.4.2.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.4.2.dev2/src/railmux.egg-info → railmux-0.4.2.dev4}/PKG-INFO +1 -1
- railmux-0.4.2.dev4/src/railmux/__init__.py +1 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/fast_display_server.py +84 -3
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/tmux_server.py +66 -6
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/transcript.py +69 -2
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fast_display_server.py +79 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_server.py +60 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_transcript.py +39 -0
- railmux-0.4.2.dev2/src/railmux/__init__.py +0 -1
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/LICENSE +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/MANIFEST.in +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/README.md +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/pyproject.toml +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/setup.cfg +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/__main__.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/background_index.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/cli.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/config.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/discovery.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/favorites.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/image_paste.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/launcher.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/local_open.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/models.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/modes.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/release_version.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/renames.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/self_update.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/session_index.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/settings.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/terminal_screen.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/app_test_harness.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_app_codex_placeholder.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_app_quit.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_background_index.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_cli.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_code_map.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_codex_index.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_config.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_config_cli.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_discovery.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_display_transport.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fast_display_selection.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_help_agent.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_image_paste.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_keymap.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_launcher.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_local_open.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_modals.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_modes.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_new_project.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_options.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_path_codec.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_release_notes.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_release_version.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_remote_config.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_renames.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_restart_state.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_running_pane.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_running_sort.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_self_update.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_session_cache.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_session_index.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_session_lease.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_settings.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_sidebar.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_status.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_statusbar.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_system_deps.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_widgets.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tools/release_notes.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.2.dev2 → railmux-0.4.2.dev4}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.2.dev4"
|
|
@@ -1747,6 +1747,83 @@ def _transcript_rows(
|
|
|
1747
1747
|
os.close(fd)
|
|
1748
1748
|
|
|
1749
1749
|
|
|
1750
|
+
_OVERLAP_STYLE_RE = re.compile(rb"\x1b\[[0-9;]*m")
|
|
1751
|
+
_OVERLAP_SPACE_RE = re.compile(r"\s+")
|
|
1752
|
+
_OVERLAP_ANCHOR_CHARS = 12
|
|
1753
|
+
_OVERLAP_SINGLE_ANCHOR_CHARS = 24
|
|
1754
|
+
_OVERLAP_SEARCH_SCREENS = 4
|
|
1755
|
+
_OVERLAP_MIN_SEARCH_ROWS = 200
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def _overlap_text(row: bytes) -> str:
|
|
1759
|
+
"""Return a row's visible text without styling or any whitespace.
|
|
1760
|
+
|
|
1761
|
+
The transcript and the provider wrap and indent the same message
|
|
1762
|
+
differently, so only the whitespace-free character stream is comparable.
|
|
1763
|
+
"""
|
|
1764
|
+
text = _OVERLAP_STYLE_RE.sub(b"", row).decode("utf-8", errors="replace")
|
|
1765
|
+
return _OVERLAP_SPACE_RE.sub("", text)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _transcript_rows_before_screen(
|
|
1769
|
+
transcript_rows: Sequence[bytes],
|
|
1770
|
+
screen_rows: Sequence[bytes],
|
|
1771
|
+
) -> int:
|
|
1772
|
+
"""Return how many transcript rows precede the live screen's content.
|
|
1773
|
+
|
|
1774
|
+
Transcript-backed history appends the live viewport after the transcript,
|
|
1775
|
+
whose tail usually holds the same newest message. Locate the screen's
|
|
1776
|
+
text in the transcript tail through distinctive anchor rows and cut the
|
|
1777
|
+
transcript where the screen begins. Two anchors must agree on that start
|
|
1778
|
+
unless one long anchor is the only candidate; without a credible match
|
|
1779
|
+
every transcript row is kept.
|
|
1780
|
+
"""
|
|
1781
|
+
total = len(transcript_rows)
|
|
1782
|
+
window = max(
|
|
1783
|
+
_OVERLAP_MIN_SEARCH_ROWS, _OVERLAP_SEARCH_SCREENS * len(screen_rows))
|
|
1784
|
+
first = max(0, total - window)
|
|
1785
|
+
row_starts: list[int] = []
|
|
1786
|
+
parts: list[str] = []
|
|
1787
|
+
offset = 0
|
|
1788
|
+
for row in transcript_rows[first:]:
|
|
1789
|
+
row_starts.append(offset)
|
|
1790
|
+
text = _overlap_text(row)
|
|
1791
|
+
parts.append(text)
|
|
1792
|
+
offset += len(text)
|
|
1793
|
+
haystack = "".join(parts)
|
|
1794
|
+
screen_offset = 0
|
|
1795
|
+
starts: list[tuple[int, int]] = []
|
|
1796
|
+
for row in screen_rows:
|
|
1797
|
+
text = _overlap_text(row)
|
|
1798
|
+
if len(text) >= _OVERLAP_ANCHOR_CHARS and haystack.count(text) == 1:
|
|
1799
|
+
starts.append((haystack.index(text) - screen_offset, len(text)))
|
|
1800
|
+
if len(starts) == 2:
|
|
1801
|
+
break
|
|
1802
|
+
screen_offset += len(text)
|
|
1803
|
+
if not starts:
|
|
1804
|
+
return total
|
|
1805
|
+
if len(starts) == 1:
|
|
1806
|
+
if starts[0][1] < _OVERLAP_SINGLE_ANCHOR_CHARS:
|
|
1807
|
+
return total
|
|
1808
|
+
else:
|
|
1809
|
+
# Allow small rendering differences (list numbering, bullets).
|
|
1810
|
+
tolerance = 16 + abs(starts[0][0]) // 20
|
|
1811
|
+
if abs(starts[0][0] - starts[1][0]) > tolerance:
|
|
1812
|
+
return total
|
|
1813
|
+
start = max(0, starts[0][0])
|
|
1814
|
+
# Keep a row that begins before the screen: it may carry text the screen
|
|
1815
|
+
# no longer shows, and a partial duplicate is better than a loss.
|
|
1816
|
+
keep = len(row_starts)
|
|
1817
|
+
for index, row_start in enumerate(row_starts):
|
|
1818
|
+
if row_start >= start:
|
|
1819
|
+
keep = index
|
|
1820
|
+
break
|
|
1821
|
+
# Drop trailing blank rows so the screen is not preceded by a gap.
|
|
1822
|
+
while keep and not parts[keep - 1]:
|
|
1823
|
+
keep -= 1
|
|
1824
|
+
return first + keep
|
|
1825
|
+
|
|
1826
|
+
|
|
1750
1827
|
def _capture_pane_history(
|
|
1751
1828
|
pyte: object,
|
|
1752
1829
|
pane: _PaneGeometry,
|
|
@@ -1852,14 +1929,18 @@ def _capture_pane_history(
|
|
|
1852
1929
|
transcript_used = transcript_entry is not None and bool(transcript_entry.rows)
|
|
1853
1930
|
if transcript_used:
|
|
1854
1931
|
assert transcript_entry is not None
|
|
1855
|
-
|
|
1932
|
+
kept = _transcript_rows_before_screen(transcript_entry.rows, current_raw)
|
|
1933
|
+
trimmed = len(transcript_entry.rows) - kept
|
|
1934
|
+
transcript_lines = transcript_entry.rows[:kept]
|
|
1856
1935
|
history_count = max(0, max_lines - pane.height)
|
|
1857
1936
|
source_lines = (
|
|
1858
1937
|
transcript_lines[-history_count:] if history_count else ()
|
|
1859
1938
|
) + current_raw
|
|
1860
1939
|
timeline_end = max(
|
|
1861
1940
|
pane.height,
|
|
1862
|
-
(transcript_entry.total_rows or len(transcript_entry.rows))
|
|
1941
|
+
(transcript_entry.total_rows or len(transcript_entry.rows))
|
|
1942
|
+
- trimmed
|
|
1943
|
+
+ pane.height,
|
|
1863
1944
|
)
|
|
1864
1945
|
else:
|
|
1865
1946
|
source_lines = rendered_raw
|
|
@@ -1910,7 +1991,7 @@ def _capture_pane_history(
|
|
|
1910
1991
|
budget_truncated
|
|
1911
1992
|
or (
|
|
1912
1993
|
transcript_entry.more_available
|
|
1913
|
-
or len(
|
|
1994
|
+
or len(transcript_lines) + pane.height > len(lines)
|
|
1914
1995
|
if transcript_used and transcript_entry is not None
|
|
1915
1996
|
else pane.history_size + pane.height > len(lines)
|
|
1916
1997
|
)
|
|
@@ -22,6 +22,8 @@ _SESSION_ID_RE = re.compile(r"[A-Za-z0-9-]{1,256}\Z")
|
|
|
22
22
|
_HISTORY_SOURCE_SCHEMA = 1
|
|
23
23
|
_TRANSCRIPT_SOURCE_SCHEMA = 1
|
|
24
24
|
_MAX_TRANSCRIPT_SOURCE_BYTES = 8192
|
|
25
|
+
_CONTINUATION_TAIL_BYTES = 4096
|
|
26
|
+
_MAX_CONTINUATION_HOPS = 8
|
|
25
27
|
_DEFAULT_DISCOVERY_TIMEOUT = 2.0
|
|
26
28
|
_WINDOWS_DISCOVERY_TIMEOUT = 5.0
|
|
27
29
|
|
|
@@ -415,11 +417,8 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
|
|
|
415
417
|
return TranscriptSource(provider, session_id, path)
|
|
416
418
|
|
|
417
419
|
|
|
418
|
-
def
|
|
419
|
-
"""Open
|
|
420
|
-
source = decode_transcript_source(raw)
|
|
421
|
-
if source is None:
|
|
422
|
-
return None
|
|
420
|
+
def _open_owned_regular_file(path: Path) -> int | None:
|
|
421
|
+
"""Open a same-user regular file without following the final symlink."""
|
|
423
422
|
flags = os.O_RDONLY
|
|
424
423
|
if hasattr(os, "O_CLOEXEC"):
|
|
425
424
|
flags |= os.O_CLOEXEC
|
|
@@ -427,7 +426,7 @@ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
|
|
|
427
426
|
flags |= os.O_NOFOLLOW
|
|
428
427
|
fd = -1
|
|
429
428
|
try:
|
|
430
|
-
fd = os.open(
|
|
429
|
+
fd = os.open(path, flags)
|
|
431
430
|
info = os.fstat(fd)
|
|
432
431
|
except OSError:
|
|
433
432
|
if fd >= 0:
|
|
@@ -443,6 +442,67 @@ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
|
|
|
443
442
|
):
|
|
444
443
|
os.close(fd)
|
|
445
444
|
return None
|
|
445
|
+
return fd
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _claude_continuation_id(fd: int) -> str | None:
|
|
449
|
+
"""Return the successor session named by a final ``continued-in`` record.
|
|
450
|
+
|
|
451
|
+
Claude Code can move a live conversation to a new session file, copying
|
|
452
|
+
the full history and ending the old file with one ``continued-in``
|
|
453
|
+
record. Only that exact final record is trusted.
|
|
454
|
+
"""
|
|
455
|
+
try:
|
|
456
|
+
size = os.fstat(fd).st_size
|
|
457
|
+
tail = os.pread(
|
|
458
|
+
fd,
|
|
459
|
+
min(size, _CONTINUATION_TAIL_BYTES),
|
|
460
|
+
max(0, size - _CONTINUATION_TAIL_BYTES),
|
|
461
|
+
)
|
|
462
|
+
except OSError:
|
|
463
|
+
return None
|
|
464
|
+
lines = tail.rstrip(b"\n").rsplit(b"\n", 1)
|
|
465
|
+
if b"continued-in" not in lines[-1]:
|
|
466
|
+
return None
|
|
467
|
+
try:
|
|
468
|
+
record = json.loads(lines[-1])
|
|
469
|
+
except (UnicodeDecodeError, ValueError):
|
|
470
|
+
return None
|
|
471
|
+
if not isinstance(record, dict) or record.get("type") != "continued-in":
|
|
472
|
+
return None
|
|
473
|
+
successor = record.get("continuedInSessionId")
|
|
474
|
+
if not isinstance(successor, str) or not _SESSION_ID_RE.fullmatch(successor):
|
|
475
|
+
return None
|
|
476
|
+
return successor
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
|
|
480
|
+
"""Open an exact same-user transcript without following the final symlink.
|
|
481
|
+
|
|
482
|
+
A Claude transcript that was continued in another session resolves to the
|
|
483
|
+
newest same-directory successor, so history keeps following the live pane.
|
|
484
|
+
"""
|
|
485
|
+
source = decode_transcript_source(raw)
|
|
486
|
+
if source is None:
|
|
487
|
+
return None
|
|
488
|
+
fd = _open_owned_regular_file(source.path)
|
|
489
|
+
if fd is None:
|
|
490
|
+
return None
|
|
491
|
+
if source.provider != "claude":
|
|
492
|
+
return source, fd
|
|
493
|
+
seen = {source.session_id}
|
|
494
|
+
for _ in range(_MAX_CONTINUATION_HOPS):
|
|
495
|
+
successor = _claude_continuation_id(fd)
|
|
496
|
+
if successor is None or successor in seen:
|
|
497
|
+
break
|
|
498
|
+
path = source.path.with_name(f"{successor}.jsonl")
|
|
499
|
+
next_fd = _open_owned_regular_file(path)
|
|
500
|
+
if next_fd is None:
|
|
501
|
+
break
|
|
502
|
+
os.close(fd)
|
|
503
|
+
fd = next_fd
|
|
504
|
+
source = TranscriptSource(source.provider, successor, path)
|
|
505
|
+
seen.add(successor)
|
|
446
506
|
return source, fd
|
|
447
507
|
|
|
448
508
|
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import io
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
|
+
import re
|
|
13
14
|
import sys
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
|
|
@@ -23,6 +24,10 @@ RED = "\033[31m"
|
|
|
23
24
|
RESET = "\033[0m"
|
|
24
25
|
CLAUDE_ACCENT = "\033[38;5;147m"
|
|
25
26
|
CLAUDE_MUTED = "\033[38;5;244m"
|
|
27
|
+
ITALIC = "\033[3m"
|
|
28
|
+
BOLD_OFF = "\033[22m"
|
|
29
|
+
ITALIC_OFF = "\033[23m"
|
|
30
|
+
FG_DEFAULT = "\033[39m"
|
|
26
31
|
|
|
27
32
|
_CODEX_SYNTHETIC_PREFIXES = (
|
|
28
33
|
"<environment_context>",
|
|
@@ -223,9 +228,70 @@ def _render_claude_tool_results(record: dict, calls: dict[str, str]):
|
|
|
223
228
|
yield _truncate(text, 500) + "\n"
|
|
224
229
|
|
|
225
230
|
|
|
226
|
-
|
|
231
|
+
_MD_FENCE_RE = re.compile(r"^\s*(```|~~~)")
|
|
232
|
+
_MD_HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
|
|
233
|
+
_MD_INLINE_RE = re.compile(
|
|
234
|
+
r"(`+)(.+?)\1"
|
|
235
|
+
r"|\*\*(?=\S)(.+?)(?<=\S)\*\*"
|
|
236
|
+
r"|__(?=\S)(.+?)(?<=\S)__"
|
|
237
|
+
r"|(?<![\w*])\*(?=[^\s*])(.+?)(?<=[^\s*])\*(?![\w*])"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _markdown_inline(line: str, *, bold: bool = False) -> str:
|
|
242
|
+
"""Style inline code, bold and italic spans like Claude's markdown view.
|
|
243
|
+
|
|
244
|
+
Only targeted "off" codes close each span, so an enclosing style (for
|
|
245
|
+
example a bold heading) survives an inner code or italic span. *bold*
|
|
246
|
+
marks an already-bold context whose nested strong spans must not end it.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def replace(match: re.Match[str]) -> str:
|
|
250
|
+
code, strong, strong_alt, emphasis = (
|
|
251
|
+
match.group(2), match.group(3), match.group(4), match.group(5))
|
|
252
|
+
if code is not None:
|
|
253
|
+
return f"{CLAUDE_ACCENT}{code}{FG_DEFAULT}"
|
|
254
|
+
if strong is not None or strong_alt is not None:
|
|
255
|
+
inner = _markdown_inline(
|
|
256
|
+
strong if strong is not None else strong_alt, bold=True)
|
|
257
|
+
return inner if bold else f"{BOLD}{inner}{BOLD_OFF}"
|
|
258
|
+
return f"{ITALIC}{_markdown_inline(emphasis, bold=bold)}{ITALIC_OFF}"
|
|
259
|
+
|
|
260
|
+
return _MD_INLINE_RE.sub(replace, line)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _markdown_lines(lines: list[str]) -> list[str]:
|
|
264
|
+
"""Approximate Claude Code's markdown rendering for sanitized text lines.
|
|
265
|
+
|
|
266
|
+
Styled lines end with a full reset: every span is already closed, and the
|
|
267
|
+
reset keeps row wrappers from carrying an ever-growing SGR prefix.
|
|
268
|
+
"""
|
|
269
|
+
rendered: list[str] = []
|
|
270
|
+
in_fence = False
|
|
271
|
+
for line in lines:
|
|
272
|
+
if _MD_FENCE_RE.match(line):
|
|
273
|
+
in_fence = not in_fence
|
|
274
|
+
continue
|
|
275
|
+
if in_fence:
|
|
276
|
+
rendered.append(line)
|
|
277
|
+
continue
|
|
278
|
+
heading = _MD_HEADING_RE.match(line)
|
|
279
|
+
if heading:
|
|
280
|
+
inner = _markdown_inline(heading.group(2), bold=True)
|
|
281
|
+
rendered.append(f"{BOLD}{inner}{RESET}")
|
|
282
|
+
continue
|
|
283
|
+
styled = _markdown_inline(line)
|
|
284
|
+
rendered.append(styled + RESET if styled != line else line)
|
|
285
|
+
return rendered
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _claude_native_block(
|
|
289
|
+
marker: str, text: str, *, colour: str, markdown: bool = False,
|
|
290
|
+
) -> str:
|
|
227
291
|
"""Render a static, non-interactive approximation of one Claude TUI block."""
|
|
228
292
|
lines = _sanitize_text(text).strip().splitlines()
|
|
293
|
+
if markdown:
|
|
294
|
+
lines = _markdown_lines(lines)
|
|
229
295
|
if not lines:
|
|
230
296
|
return ""
|
|
231
297
|
continuation = "\n".join(f" {line}" for line in lines[1:])
|
|
@@ -269,7 +335,8 @@ def _render_claude_native_assistant(
|
|
|
269
335
|
block_type = block.get("type")
|
|
270
336
|
if block_type == "text":
|
|
271
337
|
rendered = _claude_native_block(
|
|
272
|
-
"●", str(block.get("text", "")), colour=CLAUDE_ACCENT
|
|
338
|
+
"●", str(block.get("text", "")), colour=CLAUDE_ACCENT,
|
|
339
|
+
markdown=True,
|
|
273
340
|
)
|
|
274
341
|
if rendered:
|
|
275
342
|
yield rendered
|
|
@@ -1593,6 +1593,85 @@ def test_server_claude_history_uses_stable_transcript_suffix(monkeypatch):
|
|
|
1593
1593
|
assert hot.lines[-2:] == (b"live-a", b"live-b")
|
|
1594
1594
|
|
|
1595
1595
|
|
|
1596
|
+
def test_server_claude_history_drops_transcript_tail_shown_on_screen(monkeypatch):
|
|
1597
|
+
pane = fast_display_server._PaneGeometry(
|
|
1598
|
+
"%8",
|
|
1599
|
+
31,
|
|
1600
|
+
0,
|
|
1601
|
+
24,
|
|
1602
|
+
4,
|
|
1603
|
+
mouse_forwardable=True,
|
|
1604
|
+
alternate_on=True,
|
|
1605
|
+
transcript_source="exact-marker",
|
|
1606
|
+
transcript_backed=True,
|
|
1607
|
+
claude_history_policy="local",
|
|
1608
|
+
)
|
|
1609
|
+
# The transcript wraps the newest answer at the pane edge while the live
|
|
1610
|
+
# Claude screen uses a hanging indent and styling for the same text.
|
|
1611
|
+
transcript_rows = (
|
|
1612
|
+
b"\x1b[0mold question here\x1b[0m",
|
|
1613
|
+
b"\x1b[0m\x1b[0m",
|
|
1614
|
+
b"\x1b[0m\xe2\x97\x8f first answer line alph",
|
|
1615
|
+
b"a beta gamma delta\x1b[0m",
|
|
1616
|
+
b"\x1b[0m second answer line kept\x1b[0m",
|
|
1617
|
+
)
|
|
1618
|
+
live = (
|
|
1619
|
+
b"\xe2\x97\x8f first answer line",
|
|
1620
|
+
b" \x1b[1malpha beta gamma delta\x1b[0m",
|
|
1621
|
+
b" second answer line kept",
|
|
1622
|
+
b"\xe2\x9d\xaf",
|
|
1623
|
+
)
|
|
1624
|
+
monkeypatch.setattr(
|
|
1625
|
+
subprocess,
|
|
1626
|
+
"check_output",
|
|
1627
|
+
lambda *_args, **_kwargs: b"\n".join(live) + b"\n",
|
|
1628
|
+
)
|
|
1629
|
+
monkeypatch.setattr(
|
|
1630
|
+
fast_display_server,
|
|
1631
|
+
"_transcript_rows",
|
|
1632
|
+
lambda *_args, **_kwargs: fast_display_server._TranscriptCacheEntry(
|
|
1633
|
+
(1, 2, 3, 4), transcript_rows, False
|
|
1634
|
+
),
|
|
1635
|
+
)
|
|
1636
|
+
monkeypatch.setattr(
|
|
1637
|
+
fast_display_server,
|
|
1638
|
+
"_render_history_lines",
|
|
1639
|
+
lambda _pyte, lines, _width: tuple(lines),
|
|
1640
|
+
)
|
|
1641
|
+
|
|
1642
|
+
snapshot = fast_display_server._capture_pane_history(object(), pane, 1, 300)
|
|
1643
|
+
|
|
1644
|
+
assert snapshot is not None
|
|
1645
|
+
assert snapshot.lines == (transcript_rows[0],) + live
|
|
1646
|
+
assert snapshot.timeline_end == 1 + pane.height
|
|
1647
|
+
|
|
1648
|
+
|
|
1649
|
+
def test_transcript_overlap_keeps_rows_without_credible_screen_match():
|
|
1650
|
+
rows = tuple(
|
|
1651
|
+
f"repeated status line number {index % 2}".encode() for index in range(8)
|
|
1652
|
+
)
|
|
1653
|
+
unrelated = (b"nothing in common with the transcript at all", b"")
|
|
1654
|
+
repeated = (b"repeated status line number 0", b"repeated status line number 1")
|
|
1655
|
+
short = (b"tiny", b"")
|
|
1656
|
+
|
|
1657
|
+
keep = fast_display_server._transcript_rows_before_screen
|
|
1658
|
+
assert keep(rows, unrelated) == len(rows)
|
|
1659
|
+
assert keep(rows, repeated) == len(rows)
|
|
1660
|
+
assert keep(rows, short) == len(rows)
|
|
1661
|
+
assert keep((), unrelated) == 0
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def test_transcript_overlap_rejects_disagreeing_anchors():
|
|
1665
|
+
rows = (
|
|
1666
|
+
b"first distinctive anchor row",
|
|
1667
|
+
b"filler " * 20,
|
|
1668
|
+
b"second distinctive anchor row",
|
|
1669
|
+
)
|
|
1670
|
+
screen = (b"second distinctive anchor row", b"first distinctive anchor row")
|
|
1671
|
+
|
|
1672
|
+
assert fast_display_server._transcript_rows_before_screen(rows, screen) == 3
|
|
1673
|
+
|
|
1674
|
+
|
|
1596
1675
|
@pytest.mark.parametrize(
|
|
1597
1676
|
("policy", "choice_required"),
|
|
1598
1677
|
[("ask", True), ("native", False)],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import subprocess
|
|
5
6
|
from types import SimpleNamespace
|
|
@@ -519,6 +520,65 @@ def test_transcript_source_round_trip_opens_only_exact_same_user_file(tmp_path):
|
|
|
519
520
|
os.close(opened[1])
|
|
520
521
|
|
|
521
522
|
|
|
523
|
+
def _continued_in(session_id: str) -> str:
|
|
524
|
+
return json.dumps({
|
|
525
|
+
"type": "continued-in",
|
|
526
|
+
"sessionId": "previous",
|
|
527
|
+
"continuedInSessionId": session_id,
|
|
528
|
+
}) + "\n"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_claude_transcript_source_follows_continued_in_chain(tmp_path):
|
|
532
|
+
first, second, third = (
|
|
533
|
+
"47fca075-9cb8-44fb-a314-d57ef2256ad9",
|
|
534
|
+
"96fd3642-6c17-4c9b-8129-08d3d9db3e8a",
|
|
535
|
+
"02b59000-6bb4-4ec5-bdca-24f7a96b0467",
|
|
536
|
+
)
|
|
537
|
+
(tmp_path / f"{first}.jsonl").write_text(
|
|
538
|
+
'{"type":"user"}\n' + _continued_in(second))
|
|
539
|
+
(tmp_path / f"{second}.jsonl").write_text(
|
|
540
|
+
'{"type":"user"}\n' + _continued_in(third))
|
|
541
|
+
(tmp_path / f"{third}.jsonl").write_text('{"type":"assistant"}\n')
|
|
542
|
+
marker = tmux_server.encode_transcript_source(
|
|
543
|
+
"claude", first, tmp_path / f"{first}.jsonl")
|
|
544
|
+
|
|
545
|
+
opened = tmux_server.open_transcript_source(marker)
|
|
546
|
+
|
|
547
|
+
assert opened is not None
|
|
548
|
+
source, fd = opened
|
|
549
|
+
try:
|
|
550
|
+
assert source.session_id == third
|
|
551
|
+
assert source.path == tmp_path / f"{third}.jsonl"
|
|
552
|
+
assert os.read(fd, 100) == b'{"type":"assistant"}\n'
|
|
553
|
+
finally:
|
|
554
|
+
os.close(fd)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@pytest.mark.parametrize(
|
|
558
|
+
"tail",
|
|
559
|
+
[
|
|
560
|
+
_continued_in("missing-successor"),
|
|
561
|
+
_continued_in("../escape"),
|
|
562
|
+
_continued_in("47fca075-9cb8-44fb-a314-d57ef2256ad9"),
|
|
563
|
+
_continued_in("96fd3642-6c17-4c9b-8129-08d3d9db3e8a") + '{"type":"user"}\n',
|
|
564
|
+
],
|
|
565
|
+
)
|
|
566
|
+
def test_claude_transcript_source_keeps_file_without_valid_successor(
|
|
567
|
+
tmp_path, tail,
|
|
568
|
+
):
|
|
569
|
+
session_id = "47fca075-9cb8-44fb-a314-d57ef2256ad9"
|
|
570
|
+
path = tmp_path / f"{session_id}.jsonl"
|
|
571
|
+
path.write_text('{"type":"user"}\n' + tail)
|
|
572
|
+
(tmp_path / "96fd3642-6c17-4c9b-8129-08d3d9db3e8a.jsonl").write_text("{}\n")
|
|
573
|
+
marker = tmux_server.encode_transcript_source("claude", session_id, path)
|
|
574
|
+
|
|
575
|
+
opened = tmux_server.open_transcript_source(marker)
|
|
576
|
+
|
|
577
|
+
assert opened is not None
|
|
578
|
+
os.close(opened[1])
|
|
579
|
+
assert opened[0].session_id == session_id and opened[0].path == path
|
|
580
|
+
|
|
581
|
+
|
|
522
582
|
def test_codex_transcript_source_requires_matching_rollout_file(tmp_path):
|
|
523
583
|
session_id = "019fcaad-27a1-70c0-8029-8a9c7803fa6b"
|
|
524
584
|
path = tmp_path / f"rollout-2026-08-04T02-49-33-{session_id}.jsonl"
|
|
@@ -856,3 +856,42 @@ def test_transcript_unknown_prefix_does_not_force_claude_detection():
|
|
|
856
856
|
}),
|
|
857
857
|
])
|
|
858
858
|
assert "codex hello" in "".join(format_transcript(io.StringIO(text)))
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def test_claude_native_history_styles_assistant_markdown():
|
|
862
|
+
text = "\n".join([
|
|
863
|
+
"## Plan **now**",
|
|
864
|
+
"Use **bold**, `code_name` and *emphasis* in snake_case 2*3*4.",
|
|
865
|
+
"```",
|
|
866
|
+
"**literal** `kept`",
|
|
867
|
+
"```",
|
|
868
|
+
])
|
|
869
|
+
record = {
|
|
870
|
+
"type": "assistant",
|
|
871
|
+
"message": {"role": "assistant", "content": [
|
|
872
|
+
{"type": "text", "text": text},
|
|
873
|
+
]},
|
|
874
|
+
}
|
|
875
|
+
rendered = "".join(format_transcript(
|
|
876
|
+
io.StringIO(json.dumps(record) + "\n"), fmt="claude", claude_native=True
|
|
877
|
+
))
|
|
878
|
+
|
|
879
|
+
assert f"{BOLD}Plan now{RESET}" in rendered
|
|
880
|
+
assert f"{BOLD}bold\033[22m" in rendered
|
|
881
|
+
assert f"{CLAUDE_ACCENT}code_name\033[39m" in rendered
|
|
882
|
+
assert "\033[3memphasis\033[23m" in rendered
|
|
883
|
+
assert "snake_case 2*3*4." in rendered
|
|
884
|
+
assert " **literal** `kept`\n" in rendered
|
|
885
|
+
assert "```" not in rendered
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def test_claude_native_user_prompt_keeps_raw_markdown():
|
|
889
|
+
record = {
|
|
890
|
+
"type": "user",
|
|
891
|
+
"message": {"role": "user", "content": "keep **this** raw"},
|
|
892
|
+
}
|
|
893
|
+
rendered = "".join(format_transcript(
|
|
894
|
+
io.StringIO(json.dumps(record) + "\n"), fmt="claude", claude_native=True
|
|
895
|
+
))
|
|
896
|
+
|
|
897
|
+
assert "keep **this** raw" in rendered
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.2.dev2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|