railmux 0.3.2.dev2__tar.gz → 0.3.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.
Files changed (191) hide show
  1. {railmux-0.3.2.dev2/src/railmux.egg-info → railmux-0.3.2.dev4}/PKG-INFO +1 -1
  2. railmux-0.3.2.dev4/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/diagnostics.py +4 -0
  4. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/discovery.py +2 -2
  5. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_client.py +32 -22
  6. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_input.py +5 -2
  7. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/favorites.py +2 -2
  8. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/legacy_sessions.py +2 -0
  9. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mux/tmux_backend.py +2 -0
  10. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/renames.py +2 -2
  11. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/runtime_config.py +4 -0
  12. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/self_update.py +2 -0
  13. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/session_index.py +1 -1
  14. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/settings.py +1 -1
  15. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_ctl.py +14 -4
  16. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tool_panes.py +2 -0
  17. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/app.py +6 -4
  18. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4/src/railmux.egg-info}/PKG-INFO +1 -1
  19. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_fast_full_window.py +50 -17
  20. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_platform.py +106 -0
  21. railmux-0.3.2.dev2/src/railmux/__init__.py +0 -1
  22. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/LICENSE +0 -0
  23. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/MANIFEST.in +0 -0
  24. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/README.md +0 -0
  25. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/pyproject.toml +0 -0
  26. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/setup.cfg +0 -0
  27. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/__main__.py +0 -0
  28. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/atomic_file.py +0 -0
  29. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/background_index.py +0 -0
  30. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/cli.py +0 -0
  31. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/codex_index.py +0 -0
  32. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/config.py +0 -0
  33. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/config_cli.py +0 -0
  34. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/display_transport.py +0 -0
  35. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_history.py +0 -0
  36. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_protocol.py +0 -0
  37. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_server.py +0 -0
  38. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fuzzy.py +0 -0
  39. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/help_workspace.py +0 -0
  40. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/launcher.py +0 -0
  41. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/local_open.py +0 -0
  43. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/models.py +0 -0
  44. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/modes.py +0 -0
  45. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mux/__init__.py +0 -0
  47. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mux/backend.py +0 -0
  48. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/orphan_marker.py +0 -0
  49. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/pane_surface.py +0 -0
  50. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/path_codec.py +0 -0
  51. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/__init__.py +0 -0
  52. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/config_paths.py +0 -0
  53. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/console.py +0 -0
  54. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/file_security.py +0 -0
  55. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/filelock.py +0 -0
  56. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/multiplex.py +0 -0
  57. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/process.py +0 -0
  58. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/runtime_paths.py +0 -0
  59. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/remote_config.py +0 -0
  60. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/restart_state.py +0 -0
  61. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/scroll_agent.py +0 -0
  62. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/scroll_manager.py +0 -0
  63. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/selection_isolation.py +0 -0
  64. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/session_cache.py +0 -0
  65. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/setting_contracts.py +0 -0
  66. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_args.py +0 -0
  67. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_compat.py +0 -0
  68. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
  69. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_doctor.py +0 -0
  70. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/system_deps.py +0 -0
  71. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/terminal_emulator.py +0 -0
  72. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/terminal_status.py +0 -0
  73. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_binding_manager.py +0 -0
  74. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_health.py +0 -0
  75. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_server.py +0 -0
  76. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/transcript.py +0 -0
  77. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/__init__.py +0 -0
  78. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/_widgets.py +0 -0
  79. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/keymap.py +0 -0
  80. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/modals.py +0 -0
  81. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/projects_pane.py +0 -0
  82. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/running_pane.py +0 -0
  83. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/sessions_pane.py +0 -0
  84. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/sidebar.py +0 -0
  85. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/statusbar.py +0 -0
  86. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/workspace.py +0 -0
  87. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/__init__.py +0 -0
  88. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/backend.py +0 -0
  89. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/client.py +0 -0
  90. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/compositor.py +0 -0
  91. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/conpty.py +0 -0
  92. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/daemon.py +0 -0
  93. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/history_viewer.py +0 -0
  94. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/ipc.py +0 -0
  95. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/session_store.py +0 -0
  96. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/virtual_screen.py +0 -0
  97. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
  98. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
  99. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
  100. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/requires.txt +0 -0
  101. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/top_level.txt +0 -0
  102. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_agent_workspace.py +0 -0
  103. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_atomic_file.py +0 -0
  104. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_background_index.py +0 -0
  105. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_cli.py +0 -0
  106. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_delete.py +0 -0
  107. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_index.py +0 -0
  108. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_lineage.py +0 -0
  109. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_transcript.py +0 -0
  110. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_config.py +0 -0
  111. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_config_cli.py +0 -0
  112. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_denested_transport_benchmark.py +0 -0
  113. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_diagnostics.py +0 -0
  114. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_discovery.py +0 -0
  115. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_display_transport.py +0 -0
  116. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_focus_highlight.py +0 -0
  117. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_fullscreen_binding.py +0 -0
  118. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_fuzzy.py +0 -0
  119. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_help_agent.py +0 -0
  120. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_help_workspace.py +0 -0
  121. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_history_cleanup.py +0 -0
  122. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_keymap.py +0 -0
  123. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_launcher.py +0 -0
  124. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_layout_profile.py +0 -0
  125. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_legacy_sessions.py +0 -0
  126. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_local_clipboard.py +0 -0
  127. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_local_open.py +0 -0
  128. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_modals.py +0 -0
  129. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_mode_selection.py +0 -0
  130. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_modes.py +0 -0
  131. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_mouse_manager.py +0 -0
  132. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_mux_backend.py +0 -0
  133. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_new_project.py +0 -0
  134. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_options.py +0 -0
  135. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_orphan_recovery.py +0 -0
  136. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_pane_surface.py +0 -0
  137. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_path_codec.py +0 -0
  138. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_projects_pane.py +0 -0
  139. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_release_notes.py +0 -0
  140. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_release_workflow.py +0 -0
  141. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_remote_config.py +0 -0
  142. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_renames.py +0 -0
  143. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_restart_state.py +0 -0
  144. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_running_pane.py +0 -0
  145. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_running_sort.py +0 -0
  146. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_runtime_config.py +0 -0
  147. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_scroll_agent.py +0 -0
  148. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_scroll_manager.py +0 -0
  149. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_selection_isolation.py +0 -0
  150. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_self_update.py +0 -0
  151. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_session_cache.py +0 -0
  152. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_session_index.py +0 -0
  153. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_sessions_pane.py +0 -0
  154. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_setting_contracts.py +0 -0
  155. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_settings.py +0 -0
  156. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_sidebar.py +0 -0
  157. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_soft_quit.py +0 -0
  158. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_args.py +0 -0
  159. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_compat.py +0 -0
  160. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_display_benchmark.py +0 -0
  161. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
  162. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_doctor.py +0 -0
  163. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_status.py +0 -0
  164. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_statusbar.py +0 -0
  165. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_system_deps.py +0 -0
  166. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_terminal_size.py +0 -0
  167. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_terminal_status.py +0 -0
  168. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_binding_manager.py +0 -0
  169. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_ctl.py +0 -0
  170. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_health.py +0 -0
  171. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_integration.py +0 -0
  172. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_server.py +0 -0
  173. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_status.py +0 -0
  174. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tool_panes.py +0 -0
  175. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_transcript.py +0 -0
  176. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_web_demo_recorder.py +0 -0
  177. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_wheel_smoke.py +0 -0
  178. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_widgets.py +0 -0
  179. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_backend.py +0 -0
  180. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_compositor.py +0 -0
  181. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_conpty.py +0 -0
  182. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_daemon.py +0 -0
  183. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_history_viewer.py +0 -0
  184. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_ipc.py +0 -0
  185. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_real_conpty.py +0 -0
  186. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_session_store.py +0 -0
  187. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_virtual_screen.py +0 -0
  188. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_yolo_prompt.py +0 -0
  189. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tools/denested_transport_benchmark.py +0 -0
  190. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tools/release_notes.py +0 -0
  191. {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.2.dev2
3
+ Version: 0.3.2.dev4
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
@@ -0,0 +1 @@
1
+ __version__ = "0.3.2.dev4"
@@ -151,6 +151,8 @@ def _tool_diagnostic(
151
151
  env=environ,
152
152
  capture_output=True,
153
153
  text=True,
154
+ encoding="utf-8",
155
+ errors="replace",
154
156
  timeout=3,
155
157
  check=False,
156
158
  )
@@ -300,6 +302,8 @@ def collect_doctor_snapshot(
300
302
  env=effective_env,
301
303
  capture_output=True,
302
304
  text=True,
305
+ encoding="utf-8",
306
+ errors="replace",
303
307
  timeout=2,
304
308
  check=False,
305
309
  )
@@ -43,9 +43,9 @@ def _path_cache_file() -> Path:
43
43
 
44
44
  def _load_path_cache() -> dict[str, str]:
45
45
  try:
46
- data = json.loads(_path_cache_file().read_text())
46
+ data = json.loads(_path_cache_file().read_text(encoding="utf-8"))
47
47
  return data if isinstance(data, dict) else {}
48
- except (OSError, json.JSONDecodeError):
48
+ except (OSError, UnicodeError, json.JSONDecodeError):
49
49
  return {}
50
50
 
51
51
 
@@ -45,6 +45,7 @@ from railmux.fast_display_input import (
45
45
  SelectionSegment,
46
46
  SgrMouseEvent,
47
47
  TermuxTouchKeyboard,
48
+ TouchKeyboardAction,
48
49
  TerminalInputDecoder,
49
50
  is_termux_environment,
50
51
  page_key_direction,
@@ -223,6 +224,33 @@ def compact_status_row(screen: AppliedScreen) -> int | None:
223
224
  return None
224
225
 
225
226
 
227
+ def termux_prompt_touch_action(
228
+ touch_keyboard: TermuxTouchKeyboard,
229
+ event: SgrMouseEvent,
230
+ history: LocalHistoryView,
231
+ screen: AppliedScreen | None,
232
+ *,
233
+ now: float | None = None,
234
+ ) -> TouchKeyboardAction:
235
+ """Classify one translated Termux tap against live screen route authority."""
236
+ if screen is None:
237
+ return TouchKeyboardAction()
238
+ clicked_pane_id = history.pane_id_at_position(event.x - 1, event.y - 1)
239
+ cursor_pane_id = history.pane_id_at_position(
240
+ screen.cursor_x,
241
+ screen.cursor_y,
242
+ )
243
+ return touch_keyboard.pointer_event(
244
+ event,
245
+ clicked_pane_id=clicked_pane_id,
246
+ cursor_pane_id=cursor_pane_id,
247
+ cursor_y=screen.cursor_y,
248
+ pane_frozen=history.pane_is_frozen(clicked_pane_id),
249
+ navigation_row=compact_status_row(screen),
250
+ now=now,
251
+ )
252
+
253
+
226
254
  class ProbeError(RuntimeError):
227
255
  """A bounded, user-facing SSH display failure."""
228
256
 
@@ -2723,34 +2751,16 @@ def run(args: argparse.Namespace) -> int:
2723
2751
  latest_screen.cursor_x, latest_screen.cursor_y
2724
2752
  )
2725
2753
  )
2726
- clicked_pane_id = (
2727
- None
2728
- if latest_screen is None
2729
- else history.pane_id_at_position(part.x - 1, part.y - 1)
2730
- )
2731
- cursor_pane_id = (
2732
- None
2733
- if latest_screen is None
2734
- else history.pane_id_at_position(
2735
- latest_screen.cursor_x,
2736
- latest_screen.cursor_y,
2737
- )
2738
- )
2739
2754
  status_row = (
2740
2755
  compact_status_row(latest_screen)
2741
2756
  if latest_screen is not None
2742
2757
  else None
2743
2758
  )
2744
- touch_action = touch_keyboard.pointer_event(
2759
+ touch_action = termux_prompt_touch_action(
2760
+ touch_keyboard,
2745
2761
  part,
2746
- clicked_pane_id=clicked_pane_id,
2747
- cursor_pane_id=cursor_pane_id,
2748
- cursor_y=0 if latest_screen is None else latest_screen.cursor_y,
2749
- cursor_visible=(
2750
- False if latest_screen is None else latest_screen.cursor_visible
2751
- ),
2752
- pane_frozen=history.pane_is_frozen(clicked_pane_id),
2753
- navigation_row=status_row,
2762
+ history,
2763
+ latest_screen,
2754
2764
  now=time.monotonic(),
2755
2765
  )
2756
2766
  if touch_action.suspend_mouse:
@@ -129,7 +129,6 @@ class TermuxTouchKeyboard:
129
129
  clicked_pane_id: str | None,
130
130
  cursor_pane_id: str | None,
131
131
  cursor_y: int,
132
- cursor_visible: bool,
133
132
  pane_frozen: bool,
134
133
  navigation_row: int | None = None,
135
134
  now: float | None = None,
@@ -150,7 +149,6 @@ class TermuxTouchKeyboard:
150
149
  or self._active
151
150
  or self._close_reassert_pending
152
151
  or pane_frozen
153
- or not cursor_visible
154
152
  or clicked_pane_id is None
155
153
  or clicked_pane_id != cursor_pane_id
156
154
  or (navigation_row is not None and event.y == navigation_row)
@@ -158,6 +156,11 @@ class TermuxTouchKeyboard:
158
156
  or not self._is_plain_left_press(event)
159
157
  ):
160
158
  return TouchKeyboardAction()
159
+ # DEC cursor visibility is presentation state, not prompt authority.
160
+ # Providers may hide the hardware cursor while retaining its exact
161
+ # input coordinates. The verified live-agent route, matching cursor
162
+ # route, unfrozen viewport, and bounded row distance remain the
163
+ # fail-closed authority for yielding one Termux tap.
161
164
  requested_at = time.monotonic() if now is None else now
162
165
  # A rapid reopen supersedes the delayed ownership reassertion queued by
163
166
  # the previous close; Termux must receive this new native prompt tap.
@@ -29,10 +29,10 @@ class Favorites:
29
29
  if not self._path.is_file():
30
30
  return
31
31
  try:
32
- data = json.loads(self._path.read_text())
32
+ data = json.loads(self._path.read_text(encoding="utf-8"))
33
33
  if isinstance(data, list):
34
34
  self._ids = set(data)
35
- except (json.JSONDecodeError, OSError):
35
+ except (json.JSONDecodeError, OSError, UnicodeError):
36
36
  self._ids = set()
37
37
 
38
38
  def _save(self) -> None:
@@ -59,6 +59,8 @@ def discover(
59
59
  tmux_server.target_argv(target, "list-sessions", "-F", fmt),
60
60
  stderr=subprocess.DEVNULL,
61
61
  text=True,
62
+ encoding="utf-8",
63
+ errors="replace",
62
64
  timeout=timeout,
63
65
  env=None if env is None else dict(env),
64
66
  )
@@ -79,6 +79,8 @@ class TmuxBackend:
79
79
  stdout=subprocess.PIPE,
80
80
  stderr=subprocess.DEVNULL,
81
81
  text=True,
82
+ encoding="utf-8",
83
+ errors="replace",
82
84
  )
83
85
  except OSError:
84
86
  return None
@@ -32,8 +32,8 @@ class Renames:
32
32
  if not self._path.is_file():
33
33
  return
34
34
  try:
35
- data = json.loads(self._path.read_text())
36
- except (json.JSONDecodeError, OSError):
35
+ data = json.loads(self._path.read_text(encoding="utf-8"))
36
+ except (json.JSONDecodeError, OSError, UnicodeError):
37
37
  return
38
38
  if isinstance(data, dict):
39
39
  self._titles = {
@@ -125,6 +125,8 @@ def check_executable(
125
125
  env=None if environ is None else dict(environ),
126
126
  capture_output=True,
127
127
  text=True,
128
+ encoding="utf-8",
129
+ errors="replace",
128
130
  timeout=timeout,
129
131
  check=False,
130
132
  )
@@ -168,6 +170,8 @@ def check_utf8_locale(
168
170
  env=env,
169
171
  capture_output=True,
170
172
  text=True,
173
+ encoding="utf-8",
174
+ errors="replace",
171
175
  timeout=timeout,
172
176
  check=False,
173
177
  )
@@ -88,6 +88,8 @@ def fresh_process_version(*, timeout: float = 10.0) -> str | None:
88
88
  ],
89
89
  capture_output=True,
90
90
  text=True,
91
+ encoding="utf-8",
92
+ errors="replace",
91
93
  timeout=timeout,
92
94
  check=False,
93
95
  )
@@ -92,7 +92,7 @@ def _scan_session(project: Project, jsonl_path: Path) -> SessionMeta | None:
92
92
  last_stop_reason: str = "" # only set for assistant records
93
93
 
94
94
  try:
95
- with jsonl_path.open("r") as f:
95
+ with jsonl_path.open("r", encoding="utf-8", errors="replace") as f:
96
96
  for line in f:
97
97
  line = line.strip()
98
98
  if not line:
@@ -110,7 +110,7 @@ class Settings:
110
110
 
111
111
  def _read_document(self):
112
112
  try:
113
- text = self._path.read_text()
113
+ text = self._path.read_text(encoding="utf-8")
114
114
  except FileNotFoundError:
115
115
  return tomlkit.document()
116
116
  except (OSError, UnicodeError):
@@ -102,6 +102,8 @@ def _tmux_version_for_authority(
102
102
  argv,
103
103
  stderr=subprocess.DEVNULL,
104
104
  text=True,
105
+ encoding="utf-8",
106
+ errors="replace",
105
107
  timeout=2.0,
106
108
  )
107
109
  except (
@@ -1016,6 +1018,8 @@ def create_dual_pane_layout(
1016
1018
  stdout=subprocess.PIPE,
1017
1019
  stderr=subprocess.DEVNULL,
1018
1020
  text=True,
1021
+ encoding="utf-8",
1022
+ errors="replace",
1019
1023
  check=False,
1020
1024
  )
1021
1025
  except OSError:
@@ -1284,6 +1288,8 @@ def pane_process_alive(pane_id: str) -> bool:
1284
1288
  ["tmux", "display-message", "-p", "-t", pane_id, "#{pane_dead}"],
1285
1289
  capture_output=True,
1286
1290
  text=True,
1291
+ encoding="utf-8",
1292
+ errors="replace",
1287
1293
  timeout=1,
1288
1294
  )
1289
1295
  return result.returncode == 0 and result.stdout.strip() == "0"
@@ -2772,7 +2778,8 @@ def new_detached_session(name: str, cmd: str,
2772
2778
  try:
2773
2779
  dead = subprocess.run(
2774
2780
  ["tmux", "list-panes", "-t", name, "-F", "#{pane_dead}"],
2775
- capture_output=True, text=True, timeout=2,
2781
+ capture_output=True, text=True, encoding="utf-8",
2782
+ errors="replace", timeout=2,
2776
2783
  )
2777
2784
  if dead.returncode != 0:
2778
2785
  # With tmux's default ``remain-on-exit off``, an immediately
@@ -2789,7 +2796,8 @@ def new_detached_session(name: str, cmd: str,
2789
2796
  if dead.stdout.strip() == "1":
2790
2797
  capture = subprocess.run(
2791
2798
  ["tmux", "capture-pane", "-t", name, "-p", "-S", "-20"],
2792
- capture_output=True, text=True, timeout=2,
2799
+ capture_output=True, text=True, encoding="utf-8",
2800
+ errors="replace", timeout=2,
2793
2801
  )
2794
2802
  pane_content = capture.stdout.strip() if capture.returncode == 0 else ""
2795
2803
  # Clean up the dead session — it can never recover.
@@ -2913,13 +2921,15 @@ def start_detached_holder(
2913
2921
  dead = subprocess.run(
2914
2922
  ["tmux", "display-message", "-p", "-t", identity.pane_id,
2915
2923
  "#{pane_dead}"],
2916
- capture_output=True, text=True, timeout=2,
2924
+ capture_output=True, text=True, encoding="utf-8",
2925
+ errors="replace", timeout=2,
2917
2926
  )
2918
2927
  if dead.returncode == 0 and dead.stdout.strip() == "1":
2919
2928
  capture = subprocess.run(
2920
2929
  ["tmux", "capture-pane", "-t", identity.pane_id,
2921
2930
  "-p", "-S", "-20"],
2922
- capture_output=True, text=True, timeout=2,
2931
+ capture_output=True, text=True, encoding="utf-8",
2932
+ errors="replace", timeout=2,
2923
2933
  )
2924
2934
  lines = [line.strip() for line in capture.stdout.splitlines()
2925
2935
  if line.strip()] if capture.returncode == 0 else []
@@ -211,6 +211,8 @@ class ToolPaneManager:
211
211
  self._argv(*args),
212
212
  stderr=subprocess.DEVNULL,
213
213
  text=True,
214
+ encoding="utf-8",
215
+ errors="replace",
214
216
  timeout=timeout,
215
217
  ).rstrip("\n")
216
218
  return value or None
@@ -1981,7 +1981,8 @@ class App:
1981
1981
  import subprocess as _sp
1982
1982
  try:
1983
1983
  out = _sp.check_output(
1984
- ["less", "--version"], stderr=_sp.STDOUT, text=True, timeout=3)
1984
+ ["less", "--version"], stderr=_sp.STDOUT, text=True,
1985
+ encoding="utf-8", errors="replace", timeout=3)
1985
1986
  # "less 668 (GNU regular expressions)" → major version
1986
1987
  ver = int(out.strip().split()[1].split(".")[0])
1987
1988
  if ver >= 590:
@@ -6722,6 +6723,7 @@ class App:
6722
6723
  f"\t#{{{orphan_marker.OPTION_NAME}}}"
6723
6724
  f"\t#{{{_SESSION_BINDING_OPTION}}}"],
6724
6725
  stderr=_sp.DEVNULL, text=True,
6726
+ encoding="utf-8", errors="replace",
6725
6727
  )
6726
6728
  except (OSError, _sp.CalledProcessError):
6727
6729
  return False
@@ -9000,8 +9002,8 @@ class App:
9000
9002
  return True
9001
9003
  try:
9002
9004
  source_stat = history_path.stat()
9003
- lines = history_path.read_text().splitlines()
9004
- except OSError:
9005
+ lines = history_path.read_text(encoding="utf-8").splitlines()
9006
+ except (OSError, UnicodeError):
9005
9007
  return False
9006
9008
  kept = []
9007
9009
  changed = False
@@ -9081,7 +9083,7 @@ class App:
9081
9083
  import json
9082
9084
  record = json.dumps({"type": "ai-title", "aiTitle": new_title})
9083
9085
  try:
9084
- with session.jsonl_path.open("a") as f:
9086
+ with session.jsonl_path.open("a", encoding="utf-8") as f:
9085
9087
  f.write(record + "\n")
9086
9088
  except OSError:
9087
9089
  synced = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.2.dev2
3
+ Version: 0.3.2.dev4
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
@@ -88,6 +88,7 @@ from railmux.fast_display_client import (
88
88
  remote_install_help,
89
89
  screen_input_may_change_routes,
90
90
  split_local_escape,
91
+ termux_prompt_touch_action,
91
92
  )
92
93
  from railmux import fast_display_history
93
94
  from railmux.fast_display_history import (
@@ -1395,7 +1396,6 @@ def test_termux_prompt_tap_yields_mouse_until_keyboard_input():
1395
1396
  clicked_pane_id="%8",
1396
1397
  cursor_pane_id="%8",
1397
1398
  cursor_y=21,
1398
- cursor_visible=True,
1399
1399
  pane_frozen=False,
1400
1400
  now=5.0,
1401
1401
  )
@@ -1410,7 +1410,6 @@ def test_termux_prompt_tap_yields_mouse_until_keyboard_input():
1410
1410
  clicked_pane_id="%8",
1411
1411
  cursor_pane_id="%8",
1412
1412
  cursor_y=21,
1413
- cursor_visible=True,
1414
1413
  pane_frozen=False,
1415
1414
  now=5.0,
1416
1415
  ).handled
@@ -1420,6 +1419,51 @@ def test_termux_prompt_tap_yields_mouse_until_keyboard_input():
1420
1419
  assert not touch.consumes_focus_out(b"\033[O")
1421
1420
 
1422
1421
 
1422
+ def test_termux_live_route_tap_yields_mouse_when_provider_hides_dec_cursor():
1423
+ history = LocalHistoryView()
1424
+ assert history.begin_prefetch(1.0) is not None
1425
+ request_id = history.prefetch_pending_id
1426
+ assert request_id is not None
1427
+ route = HistorySnapshot(
1428
+ request_id,
1429
+ "%8",
1430
+ 30,
1431
+ 0,
1432
+ 50,
1433
+ 24,
1434
+ (b"",) * 24,
1435
+ )
1436
+ history.accept_prefetch(HistoryBatch(request_id, (route,)))
1437
+ screen = AppliedScreen(
1438
+ width=80,
1439
+ height=24,
1440
+ cursor_x=40,
1441
+ cursor_y=21,
1442
+ cursor_visible=False,
1443
+ terminal_modes=TerminalMode.FOCUS_EVENTS,
1444
+ rows=(b"",) * 24,
1445
+ changed_rows=(),
1446
+ clear=False,
1447
+ )
1448
+ touch = TermuxTouchKeyboard(enabled=True)
1449
+
1450
+ action = termux_prompt_touch_action(
1451
+ touch,
1452
+ SgrMouseEvent(b"press", 0, 41, 22, True),
1453
+ history,
1454
+ screen,
1455
+ now=5.0,
1456
+ )
1457
+
1458
+ stream = io.BytesIO()
1459
+ surface = TerminalSurface(stream, mouse_hover=False)
1460
+ surface.start()
1461
+ if action.suspend_mouse:
1462
+ surface.suspend_mouse()
1463
+ assert action.handled and action.show_hint
1464
+ assert stream.getvalue().endswith(b"\033[?1002l\033[?1006l")
1465
+
1466
+
1423
1467
  def test_termux_focus_reassertion_requires_remote_focus_events():
1424
1468
  screen = AppliedScreen(
1425
1469
  width=20,
@@ -1452,24 +1496,21 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
1452
1496
  clicked_pane_id="%8",
1453
1497
  cursor_pane_id="%8",
1454
1498
  cursor_y=21,
1455
- cursor_visible=True,
1456
1499
  pane_frozen=False,
1457
1500
  now=5.0,
1458
1501
  ).handled
1459
1502
 
1460
1503
  touch = TermuxTouchKeyboard(enabled=True, timeout=10.0)
1461
- for clicked, cursor, visible, frozen, y in (
1462
- ("%9", "%8", True, False, 21),
1463
- ("%8", "%8", False, False, 21),
1464
- ("%8", "%8", True, True, 21),
1465
- ("%8", "%8", True, False, 18),
1504
+ for clicked, cursor, frozen, y in (
1505
+ ("%9", "%8", False, 21),
1506
+ ("%8", "%8", True, 21),
1507
+ ("%8", "%8", False, 18),
1466
1508
  ):
1467
1509
  assert not touch.pointer_event(
1468
1510
  press,
1469
1511
  clicked_pane_id=clicked,
1470
1512
  cursor_pane_id=cursor,
1471
1513
  cursor_y=y,
1472
- cursor_visible=visible,
1473
1514
  pane_frozen=frozen,
1474
1515
  now=5.0,
1475
1516
  ).handled
@@ -1479,7 +1520,6 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
1479
1520
  clicked_pane_id="%8",
1480
1521
  cursor_pane_id="%8",
1481
1522
  cursor_y=21,
1482
- cursor_visible=True,
1483
1523
  pane_frozen=False,
1484
1524
  now=5.0,
1485
1525
  )
@@ -1495,7 +1535,6 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
1495
1535
  clicked_pane_id="%8",
1496
1536
  cursor_pane_id="%8",
1497
1537
  cursor_y=21,
1498
- cursor_visible=True,
1499
1538
  pane_frozen=False,
1500
1539
  now=15.2,
1501
1540
  ).handled
@@ -1504,7 +1543,6 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
1504
1543
  clicked_pane_id="%8",
1505
1544
  cursor_pane_id="%8",
1506
1545
  cursor_y=21,
1507
- cursor_visible=True,
1508
1546
  pane_frozen=False,
1509
1547
  now=15.2,
1510
1548
  ).handled
@@ -1525,7 +1563,6 @@ def test_termux_compact_navigation_never_yields_touch_to_soft_keyboard():
1525
1563
  clicked_pane_id="%8",
1526
1564
  cursor_pane_id="%8",
1527
1565
  cursor_y=21,
1528
- cursor_visible=True,
1529
1566
  pane_frozen=False,
1530
1567
  navigation_row=22,
1531
1568
  now=5.0,
@@ -1543,7 +1580,6 @@ def test_termux_keyboard_projection_state_has_a_bounded_fallback():
1543
1580
  clicked_pane_id="%8",
1544
1581
  cursor_pane_id="%8",
1545
1582
  cursor_y=21,
1546
- cursor_visible=True,
1547
1583
  pane_frozen=False,
1548
1584
  now=5.0,
1549
1585
  )
@@ -1571,7 +1607,6 @@ def test_termux_keyboard_close_queues_one_delayed_mouse_reassert():
1571
1607
  clicked_pane_id="%8",
1572
1608
  cursor_pane_id="%8",
1573
1609
  cursor_y=21,
1574
- cursor_visible=True,
1575
1610
  pane_frozen=False,
1576
1611
  now=5.0,
1577
1612
  )
@@ -1593,7 +1628,6 @@ def test_termux_rapid_keyboard_reopen_cancels_delayed_reassert():
1593
1628
  "clicked_pane_id": "%8",
1594
1629
  "cursor_pane_id": "%8",
1595
1630
  "cursor_y": 21,
1596
- "cursor_visible": True,
1597
1631
  "pane_frozen": False,
1598
1632
  }
1599
1633
  touch.pointer_event(press, now=5.0, **fields)
@@ -1611,7 +1645,6 @@ def test_termux_prompt_tap_times_out_when_keyboard_does_not_open():
1611
1645
  clicked_pane_id="%8",
1612
1646
  cursor_pane_id="%8",
1613
1647
  cursor_y=21,
1614
- cursor_visible=True,
1615
1648
  pane_frozen=False,
1616
1649
  now=5.0,
1617
1650
  )
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  import subprocess
3
4
  import sys
@@ -92,6 +93,111 @@ def test_windows_npm_provider_shim_uses_one_bounded_cmd_command():
92
93
  assert '"session with spaces"' in argv[4]
93
94
 
94
95
 
96
+ def test_utf8_user_state_survives_a_non_utf8_process_locale(tmp_path):
97
+ """UTF-8 persistence must not inherit CP936/ANSI locale decoding."""
98
+ config_roots = {
99
+ tmp_path / ".config" / "railmux",
100
+ tmp_path / ".config" / "Railmux",
101
+ }
102
+ for config_root in config_roots:
103
+ config_root.mkdir(parents=True, exist_ok=True)
104
+ (config_root / "config.toml").write_bytes(
105
+ '# 中文注释\n[updates]\nauto_update = "never"\n'.encode("utf-8")
106
+ )
107
+ (config_root / "favorites.json").write_bytes(
108
+ json.dumps(["会话"], ensure_ascii=False).encode("utf-8")
109
+ )
110
+ (config_root / "renames.json").write_bytes(
111
+ json.dumps({"会话": "中文标题"}, ensure_ascii=False).encode("utf-8")
112
+ )
113
+ (config_root / "path-cache.json").write_bytes(
114
+ json.dumps(
115
+ {"编码目录": str(tmp_path / "项目")}, ensure_ascii=False
116
+ ).encode("utf-8")
117
+ )
118
+ project_dir = tmp_path / ".claude" / "projects" / "encoded"
119
+ project_dir.mkdir(parents=True)
120
+ session_id = "11111111-1111-1111-1111-111111111111"
121
+ session_path = project_dir / f"{session_id}.jsonl"
122
+ records = (
123
+ {"type": "ai-title", "aiTitle": "会话标题"},
124
+ {"type": "user", "message": {"content": "中文问题"}},
125
+ {
126
+ "type": "assistant",
127
+ "message": {
128
+ "content": "中文回答",
129
+ "stop_reason": "end_turn",
130
+ "usage": {"input_tokens": 1, "output_tokens": 1},
131
+ },
132
+ },
133
+ )
134
+ session_path.write_bytes(
135
+ ("\n".join(json.dumps(row, ensure_ascii=False) for row in records) + "\n").encode(
136
+ "utf-8"
137
+ )
138
+ )
139
+ history = tmp_path / ".claude" / "history.jsonl"
140
+ history.write_bytes(
141
+ (
142
+ json.dumps(
143
+ {"sessionId": "remove", "display": "删除我"}, ensure_ascii=False
144
+ )
145
+ + "\n"
146
+ + json.dumps(
147
+ {"sessionId": "keep", "display": "保留我"}, ensure_ascii=False
148
+ )
149
+ + "\n"
150
+ ).encode("utf-8")
151
+ )
152
+ script = r"""
153
+ import json
154
+ import locale
155
+ import sys
156
+ from pathlib import Path
157
+ from railmux.discovery import _load_path_cache
158
+ from railmux.favorites import Favorites
159
+ from railmux.models import Project
160
+ from railmux.renames import Renames
161
+ from railmux.session_index import _scan_session
162
+ from railmux.settings import Settings
163
+ from railmux.ui.app import App
164
+
165
+ home = Path(sys.argv[1])
166
+ assert locale.getpreferredencoding(False).upper() not in {'UTF-8', 'UTF8'}
167
+ assert Settings().update_policy == 'never'
168
+ assert Favorites().is_favorite('\u4f1a\u8bdd')
169
+ assert Renames().get('\u4f1a\u8bdd') == '\u4e2d\u6587\u6807\u9898'
170
+ assert _load_path_cache()['\u7f16\u7801\u76ee\u5f55'].endswith('\u9879\u76ee')
171
+ project = Project(home / '\u9879\u76ee', 'encoded', home / '.claude' / 'projects' / 'encoded', 1, 0)
172
+ session = _scan_session(project, project.claude_dir / '11111111-1111-1111-1111-111111111111.jsonl')
173
+ assert session is not None and session.title == '\u4f1a\u8bdd\u6807\u9898'
174
+ assert App._remove_from_history('remove', claude_home=home / '.claude')
175
+ rows = [json.loads(line) for line in (home / '.claude' / 'history.jsonl').read_bytes().decode('utf-8').splitlines()]
176
+ assert rows == [{'sessionId': 'keep', 'display': '\u4fdd\u7559\u6211'}]
177
+ print('ok')
178
+ """
179
+ result = subprocess.run(
180
+ [sys.executable, "-c", script, str(tmp_path)],
181
+ env={
182
+ **os.environ,
183
+ "APPDATA": str(tmp_path / ".config"),
184
+ "HOME": str(tmp_path),
185
+ "LC_ALL": "C",
186
+ "PYTHONCOERCECLOCALE": "0",
187
+ "PYTHONUTF8": "0",
188
+ "PYTHONPATH": str(Path(__file__).parents[1] / "src"),
189
+ },
190
+ capture_output=True,
191
+ text=True,
192
+ encoding="utf-8",
193
+ errors="replace",
194
+ check=False,
195
+ )
196
+
197
+ assert result.returncode == 0, result.stderr
198
+ assert result.stdout.strip() == "ok"
199
+
200
+
95
201
  def test_posix_provider_launch_remains_direct_argv():
96
202
  assert provider_argv("/opt/bin/codex", ("resume", "abc"), windows=False) == (
97
203
  "/opt/bin/codex",
@@ -1 +0,0 @@
1
- __version__ = "0.3.2.dev2"
File without changes
File without changes
File without changes
File without changes