railmux 0.3.3.dev4__tar.gz → 0.3.3.dev5__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 (160) hide show
  1. {railmux-0.3.3.dev4/src/railmux.egg-info → railmux-0.3.3.dev5}/PKG-INFO +6 -2
  2. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/README.md +5 -1
  3. railmux-0.3.3.dev5/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/app.py +275 -123
  5. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5/src/railmux.egg-info}/PKG-INFO +6 -2
  6. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_codex_delete.py +134 -39
  7. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_statusbar.py +16 -0
  8. railmux-0.3.3.dev4/src/railmux/__init__.py +0 -1
  9. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/LICENSE +0 -0
  10. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/MANIFEST.in +0 -0
  11. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/pyproject.toml +0 -0
  12. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/setup.cfg +0 -0
  13. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/__main__.py +0 -0
  14. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/atomic_file.py +0 -0
  15. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/background_index.py +0 -0
  16. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/cli.py +0 -0
  17. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/codex_index.py +0 -0
  18. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/config.py +0 -0
  19. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/config_cli.py +0 -0
  20. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/diagnostics.py +0 -0
  21. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/discovery.py +0 -0
  22. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/display_transport.py +0 -0
  23. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/fast_display_client.py +0 -0
  24. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/fast_display_history.py +0 -0
  25. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/fast_display_input.py +0 -0
  26. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/fast_display_protocol.py +0 -0
  27. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/fast_display_server.py +0 -0
  28. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/favorites.py +0 -0
  29. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/fuzzy.py +0 -0
  30. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/help_workspace.py +0 -0
  31. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/launcher.py +0 -0
  32. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/legacy_sessions.py +0 -0
  33. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/local_clipboard.py +0 -0
  34. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/local_open.py +0 -0
  35. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/models.py +0 -0
  36. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/modes.py +0 -0
  37. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/mouse_manager.py +0 -0
  38. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/orphan_marker.py +0 -0
  39. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/pane_surface.py +0 -0
  40. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/path_codec.py +0 -0
  41. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/remote_config.py +0 -0
  42. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/renames.py +0 -0
  43. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/restart_state.py +0 -0
  44. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/runtime_config.py +0 -0
  45. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/scroll_agent.py +0 -0
  46. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/scroll_manager.py +0 -0
  47. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/selection_isolation.py +0 -0
  48. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/self_update.py +0 -0
  49. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/session_cache.py +0 -0
  50. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/session_index.py +0 -0
  51. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/setting_contracts.py +0 -0
  52. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/settings.py +0 -0
  53. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ssh_args.py +0 -0
  54. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ssh_compat.py +0 -0
  55. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
  56. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ssh_doctor.py +0 -0
  57. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/system_deps.py +0 -0
  58. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/terminal_status.py +0 -0
  59. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/tmux_binding_manager.py +0 -0
  60. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/tmux_ctl.py +0 -0
  61. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/tmux_health.py +0 -0
  62. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/tmux_server.py +0 -0
  63. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/tool_panes.py +0 -0
  64. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/transcript.py +0 -0
  65. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/__init__.py +0 -0
  66. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/_widgets.py +0 -0
  67. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/keymap.py +0 -0
  68. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/modals.py +0 -0
  69. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/projects_pane.py +0 -0
  70. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/running_pane.py +0 -0
  71. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/sessions_pane.py +0 -0
  72. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/sidebar.py +0 -0
  73. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/statusbar.py +0 -0
  74. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux/ui/workspace.py +0 -0
  75. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux.egg-info/SOURCES.txt +0 -0
  76. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
  77. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
  78. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux.egg-info/requires.txt +0 -0
  79. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/src/railmux.egg-info/top_level.txt +0 -0
  80. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_agent_workspace.py +0 -0
  81. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_atomic_file.py +0 -0
  82. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_background_index.py +0 -0
  83. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_cli.py +0 -0
  84. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_codex_index.py +0 -0
  85. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_codex_lineage.py +0 -0
  86. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_codex_transcript.py +0 -0
  87. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_config.py +0 -0
  88. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_config_cli.py +0 -0
  89. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_denested_transport_benchmark.py +0 -0
  90. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_diagnostics.py +0 -0
  91. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_discovery.py +0 -0
  92. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_display_transport.py +0 -0
  93. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_fast_full_window.py +0 -0
  94. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_focus_highlight.py +0 -0
  95. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_fullscreen_binding.py +0 -0
  96. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_fuzzy.py +0 -0
  97. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_help_agent.py +0 -0
  98. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_help_workspace.py +0 -0
  99. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_history_cleanup.py +0 -0
  100. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_keymap.py +0 -0
  101. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_launcher.py +0 -0
  102. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_layout_profile.py +0 -0
  103. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_legacy_sessions.py +0 -0
  104. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_local_clipboard.py +0 -0
  105. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_local_open.py +0 -0
  106. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_modals.py +0 -0
  107. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_mode_selection.py +0 -0
  108. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_modes.py +0 -0
  109. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_mouse_manager.py +0 -0
  110. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_new_project.py +0 -0
  111. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_options.py +0 -0
  112. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_orphan_recovery.py +0 -0
  113. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_pane_surface.py +0 -0
  114. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_path_codec.py +0 -0
  115. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_projects_pane.py +0 -0
  116. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_release_notes.py +0 -0
  117. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_release_workflow.py +0 -0
  118. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_remote_config.py +0 -0
  119. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_renames.py +0 -0
  120. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_restart_state.py +0 -0
  121. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_running_pane.py +0 -0
  122. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_running_sort.py +0 -0
  123. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_runtime_config.py +0 -0
  124. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_scroll_agent.py +0 -0
  125. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_scroll_manager.py +0 -0
  126. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_selection_isolation.py +0 -0
  127. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_self_update.py +0 -0
  128. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_session_cache.py +0 -0
  129. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_session_index.py +0 -0
  130. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_sessions_pane.py +0 -0
  131. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_setting_contracts.py +0 -0
  132. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_settings.py +0 -0
  133. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_sidebar.py +0 -0
  134. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_soft_quit.py +0 -0
  135. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_ssh_args.py +0 -0
  136. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_ssh_compat.py +0 -0
  137. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_ssh_display_benchmark.py +0 -0
  138. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
  139. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_ssh_doctor.py +0 -0
  140. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_status.py +0 -0
  141. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_system_deps.py +0 -0
  142. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_terminal_size.py +0 -0
  143. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_terminal_status.py +0 -0
  144. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_termux_local_touch.py +0 -0
  145. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tmux_binding_manager.py +0 -0
  146. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tmux_ctl.py +0 -0
  147. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tmux_health.py +0 -0
  148. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tmux_integration.py +0 -0
  149. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tmux_server.py +0 -0
  150. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tmux_status.py +0 -0
  151. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_tool_panes.py +0 -0
  152. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_transcript.py +0 -0
  153. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/tools/release_notes.py +0 -0
  160. {railmux-0.3.3.dev4 → railmux-0.3.3.dev5}/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.dev4
3
+ Version: 0.3.3.dev5
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
@@ -204,7 +204,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
204
204
  | `r` | Rename the focused session |
205
205
  | `s` | Toggle star — starred sessions pinned to top with ⭐ |
206
206
  | `k` | Kill the running agent process (keeps session file) |
207
- | `d` | Delete the focused session (prompts for confirmation) |
207
+ | `d` | Delete the focused session (confirms, then finishes in background) |
208
208
  | `t` | Open or return to the managed terminal below the Target agent |
209
209
  | `T` | Return to that agent's managed Vim viewer, when open |
210
210
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
@@ -242,6 +242,10 @@ The rename popup starts with the current title pre-filled. Press
242
242
  `Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
243
243
  to cancel.
244
244
 
245
+ After a delete is confirmed, its popup closes immediately. The bottom-right
246
+ status shows **Deleting…** while Railmux stops the exact provider session and
247
+ removes its history in the background, then reports the final result.
248
+
245
249
  Press `/` in Projects, Sessions, or Running to filter that section. Active
246
250
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
247
251
  shows the current value. Press `Ctrl-U` in the filter editor to clear it, then
@@ -162,7 +162,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
162
162
  | `r` | Rename the focused session |
163
163
  | `s` | Toggle star — starred sessions pinned to top with ⭐ |
164
164
  | `k` | Kill the running agent process (keeps session file) |
165
- | `d` | Delete the focused session (prompts for confirmation) |
165
+ | `d` | Delete the focused session (confirms, then finishes in background) |
166
166
  | `t` | Open or return to the managed terminal below the Target agent |
167
167
  | `T` | Return to that agent's managed Vim viewer, when open |
168
168
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
@@ -200,6 +200,10 @@ The rename popup starts with the current title pre-filled. Press
200
200
  `Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
201
201
  to cancel.
202
202
 
203
+ After a delete is confirmed, its popup closes immediately. The bottom-right
204
+ status shows **Deleting…** while Railmux stops the exact provider session and
205
+ removes its history in the background, then reports the final result.
206
+
203
207
  Press `/` in Projects, Sessions, or Running to filter that section. Active
204
208
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
205
209
  shows the current value. Press `Ctrl-U` in the filter editor to clear it, then
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3.dev5"
@@ -387,6 +387,35 @@ class _Running:
387
387
  return None if self.is_placeholder else self.key
388
388
 
389
389
 
390
+ @dataclass(frozen=True)
391
+ class _DeleteTask:
392
+ """Immutable destructive work handed from Urwid to one worker thread."""
393
+
394
+ session_id: str | None
395
+ jsonl_path: Path | None
396
+ tmux_name: str | None
397
+ label: str
398
+ session_type: str
399
+ pane_identity: tmux_ctl.PaneIdentity | None
400
+ writer_pids: tuple[int, ...]
401
+ lineage_ids: frozenset[str]
402
+ claude_home: Path
403
+
404
+
405
+ @dataclass(frozen=True)
406
+ class _DeleteResult:
407
+ """Worker result installed atomically by the next Urwid refresh."""
408
+
409
+ task: _DeleteTask
410
+ status_text: str
411
+ status_level: str = "info"
412
+ remove_running: bool = False
413
+ views_changed: bool = False
414
+ claude_path_changed: bool = False
415
+ invalidate_codex: bool = False
416
+ deleted_codex_ids: frozenset[str] = frozenset()
417
+
418
+
390
419
  class _CloseOnClickOverlay(urwid.Overlay):
391
420
  """An ``urwid.Overlay`` that calls *on_click_outside* when the user
392
421
  left-clicks anywhere outside the overlay's area.
@@ -885,6 +914,14 @@ class App:
885
914
  self._mode_refresh_result: (
886
915
  tuple[list[Project] | None, str | None] | None
887
916
  ) = None
917
+ # Destructive provider cleanup may include a bounded process-exit wait
918
+ # and several 30-second provider CLI calls. Urwid prepares display/tmux
919
+ # ownership on its thread, then this single worker performs only the
920
+ # blocking exact kill and filesystem/provider work. Results are applied
921
+ # by _refresh, never by the worker.
922
+ self._delete_lock = threading.Lock()
923
+ self._delete_thread: threading.Thread | None = None
924
+ self._delete_result: _DeleteResult | None = None
888
925
 
889
926
  projects = list_projects(claude_home)
890
927
  self._project_snapshot = projects
@@ -8093,6 +8130,10 @@ class App:
8093
8130
 
8094
8131
  def _refresh(self) -> None:
8095
8132
  """Refresh every visible view against one coherent Codex generation."""
8133
+ # Install completed deletion tombstones before pinning the immutable
8134
+ # Codex generation, so the completion frame cannot briefly redraw a
8135
+ # provider row that the worker has already removed.
8136
+ self._consume_delete_result()
8096
8137
  index = getattr(self, "_codex_index", None)
8097
8138
  if isinstance(index, BackgroundCodexIndex):
8098
8139
  index.begin_read()
@@ -9105,13 +9146,22 @@ class App:
9105
9146
  label: str = "",
9106
9147
  session_type: str = "claude",
9107
9148
  identity_token: str | None = None) -> None:
9108
- """Provider-aware session cleanup: kill tmux → remove backing store →
9109
- refresh UI.
9149
+ """Prepare an exact cleanup transaction, then run it off the UI thread.
9150
+
9151
+ Display ownership and immutable tmux identity are validated here on the
9152
+ Urwid thread. The worker receives only frozen values and never mutates
9153
+ widgets, the Running registry, or provider indexes directly.
9154
+ """
9155
+ if self._delete_pending():
9156
+ self._set_status("A session deletion is already in progress.", "warn")
9157
+ return
9158
+
9159
+ # Paint the closed confirmation and durable progress message before
9160
+ # even the bounded tmux preparation probes run. This makes Enter feel
9161
+ # immediate on slow homes and provider installations.
9162
+ self._set_status("Deleting…", "info", force=True)
9163
+ self._redraw_focus_state_now()
9110
9164
 
9111
- Claude sessions unlink the JSONL and clean the Claude session-env +
9112
- history index. Codex sessions are deleted through ``codex delete``
9113
- against the resolved CODEX_HOME and never touch any Claude path (#1)."""
9114
- # 1. Kill the detached tmux session first (avoid race conditions).
9115
9165
  if tmux_name is None and session_id is not None:
9116
9166
  r = self._by_session_id(session_id)
9117
9167
  tmux_name = r.tmux_name if r else None
@@ -9139,125 +9189,102 @@ class App:
9139
9189
  writer_pids = tmux_ctl.session_process_ids(tmux_name)
9140
9190
  if exact_pane is not None:
9141
9191
  # Returning a swap-displayed pane changes its current session;
9142
- # refresh and require the recorded home identity before kill.
9192
+ # refresh and require the recorded home identity before the
9193
+ # immutable identity is handed to the worker.
9143
9194
  exact_pane = self._exact_running_pane(owned)
9144
- killed = bool(
9145
- exact_pane is not None
9146
- and orphan_marker.same_live_tmux(owned.orphan, exact_pane)
9147
- and tmux_ctl.kill_session_identity(exact_pane)
9148
- )
9149
9195
  else:
9150
- killed = tmux_ctl.kill_session(tmux_name)
9151
- # Never remove a rollout while its writer may still be alive. A
9152
- # concurrent exit is fine; a session that still exists after the
9153
- # failed kill is a hard stop for both Claude and Codex deletion.
9154
- still_alive = (
9155
- self._exact_running_pane(owned) is not None
9156
- if owned is not None and owned.orphan is not None
9157
- else tmux_ctl.session_exists(tmux_name)
9158
- )
9159
- if not killed and still_alive:
9160
- self._set_status(
9161
- f"failed to stop {tmux_name}; nothing was deleted", "error")
9162
- return
9163
- if writer_pids and not tmux_ctl.wait_for_processes_exit(writer_pids):
9196
+ topology = tmux_ctl.session_topology(tmux_name)
9197
+ exact_pane = (
9198
+ topology.single_live_pane
9199
+ if topology is not None else None
9200
+ )
9201
+ if (exact_pane is None
9202
+ or (owned is not None and owned.orphan is not None
9203
+ and not orphan_marker.same_live_tmux(
9204
+ owned.orphan, exact_pane))):
9164
9205
  self._set_status(
9165
- f"{tmux_name} is still shutting down; nothing was deleted",
9206
+ f"could not confirm the exact tmux identity for "
9207
+ f"{tmux_name}; nothing was deleted",
9166
9208
  "error",
9167
9209
  )
9168
9210
  return
9211
+ is_real = bool(session_id) and not session_id.startswith("__new__-")
9212
+ lineage_ids = frozenset(
9213
+ self._codex_lineage_ids(session_id)
9214
+ if session_type == "codex" and is_real
9215
+ and session_id is not None else ()
9216
+ )
9217
+ task = _DeleteTask(
9218
+ session_id=session_id,
9219
+ jsonl_path=jsonl_path,
9220
+ tmux_name=tmux_name,
9221
+ label=label,
9222
+ session_type=session_type,
9223
+ pane_identity=exact_pane,
9224
+ writer_pids=writer_pids,
9225
+ lineage_ids=lineage_ids,
9226
+ claude_home=getattr(
9227
+ self, "_claude_home", Path.home() / ".claude"),
9228
+ )
9229
+ lock = self._delete_lock
9169
9230
 
9170
- if session_type == "codex":
9171
- self._cleanup_codex_session(session_id, tmux_name, label)
9172
- return
9173
-
9174
- # 2. Remove from our running-session registry.
9175
- self._forget_running(session_id, tmux_name)
9176
-
9177
- # 3. Delete the JSONL file (conversation history). Do not report a
9178
- # successful deletion when the filesystem rejected the unlink.
9179
- if jsonl_path is not None:
9180
- try:
9181
- jsonl_path.unlink(missing_ok=True)
9182
- invalidate_session(jsonl_path)
9183
- except OSError as exc:
9184
- self._session_cache.invalidate()
9185
- self._invalidate_project_snapshot()
9186
- self._refresh()
9187
- self._set_status(f"failed to delete {jsonl_path}: {exc}", "error")
9188
- return
9189
-
9190
- # 4. Remove Claude's session-env directory (session metadata).
9191
- if session_id is not None and not session_id.startswith("__new__-"):
9192
- claude_home = getattr(
9193
- self, "_claude_home", Path.home() / ".claude")
9194
- env_dir = claude_home / "session-env" / session_id
9195
- if env_dir.is_dir():
9196
- shutil.rmtree(env_dir, ignore_errors=True)
9197
-
9198
- # 5. Remove from Claude's history index so it doesn't recreate a
9199
- # metadata stub (Claude rebuilds missing JSONLs from this index).
9200
- history_ok = True
9201
- if session_id is not None and not session_id.startswith("__new__-"):
9202
- claude_home = getattr(
9203
- self, "_claude_home", Path.home() / ".claude")
9204
- history_ok = self._remove_from_history(
9205
- session_id, claude_home=claude_home) is not False
9206
-
9207
- # A writer outside the captured tmux process tree could race the first
9208
- # unlink. Verify once after history cleanup; a surviving file means the
9209
- # requested delete did not complete and must not be reported as such.
9210
- if jsonl_path is not None and jsonl_path.exists():
9231
+ def _worker() -> None:
9211
9232
  try:
9212
- # Disappearance between exists() and unlink() is already the
9213
- # desired end state, not a deletion failure.
9214
- jsonl_path.unlink(missing_ok=True)
9215
- invalidate_session(jsonl_path)
9216
- except OSError as exc:
9217
- self._session_cache.invalidate()
9218
- self._invalidate_project_snapshot()
9219
- self._refresh()
9220
- self._set_status(
9221
- f"session was recreated and could not be deleted: {exc}",
9222
- "error",
9233
+ result = self._execute_delete_task(task)
9234
+ except Exception as exc:
9235
+ result = _DeleteResult(
9236
+ task=task,
9237
+ status_text=f"failed to delete {label}: {exc}",
9238
+ status_level="error",
9223
9239
  )
9224
- return
9240
+ with lock:
9241
+ self._delete_result = result
9225
9242
 
9226
- # 6. Invalidate caches and refresh so the UI reflects the deletion
9227
- # immediately — no stale rows that point to deleted sessions.
9228
- self._session_cache.invalidate()
9229
- self._invalidate_project_snapshot()
9230
- self._refresh()
9243
+ thread = threading.Thread(
9244
+ target=_worker,
9245
+ name="railmux-session-delete",
9246
+ daemon=True,
9247
+ )
9248
+ self._delete_thread = thread
9249
+ thread.start()
9231
9250
 
9232
- deleted = (session_id is not None
9233
- and not session_id.startswith("__new__-")
9234
- and jsonl_path is not None)
9235
- if not history_ok:
9236
- self._set_status(
9237
- f"Deleted: {label} (history index cleanup failed)", "warn")
9238
- else:
9239
- self._set_status(f"{'Deleted' if deleted else 'Killed'}: {label}")
9251
+ def _execute_delete_task(self, task: _DeleteTask) -> _DeleteResult:
9252
+ """Perform blocking exact kill and provider cleanup without UI state."""
9253
+ if task.pane_identity is not None:
9254
+ killed = tmux_ctl.kill_session_identity(task.pane_identity)
9255
+ still_alive = tmux_ctl.exact_pane_alive(task.pane_identity)
9256
+ if not killed and still_alive:
9257
+ return _DeleteResult(
9258
+ task=task,
9259
+ status_text=(f"failed to stop {task.tmux_name}; "
9260
+ "nothing was deleted"),
9261
+ status_level="error",
9262
+ )
9263
+ if (task.writer_pids
9264
+ and not tmux_ctl.wait_for_processes_exit(task.writer_pids)):
9265
+ return _DeleteResult(
9266
+ task=task,
9267
+ status_text=(f"{task.tmux_name} is still shutting down; "
9268
+ "nothing was deleted"),
9269
+ status_level="error",
9270
+ views_changed=True,
9271
+ )
9240
9272
 
9241
- def _cleanup_codex_session(self, session_id: str | None,
9242
- tmux_name: str | None, label: str) -> None:
9243
- """Codex arm of ``_cleanup_session`` (tmux already killed).
9273
+ if task.session_type == "codex":
9274
+ return self._execute_codex_delete(task)
9275
+ return self._execute_claude_delete(task)
9244
9276
 
9245
- A real rollout and its rewind lineage are removed via
9246
- ``codex delete --force``. A total failure leaves the registry and index
9247
- untouched; a partial failure refreshes the surviving records and
9248
- reports exact counts. A placeholder (no resolved UUID yet) has no
9249
- rollout on disk, so killing the tmux session is the whole operation."""
9277
+ def _execute_codex_delete(self, task: _DeleteTask) -> _DeleteResult:
9278
+ """Delete one immutable Codex rewind lineage in the worker."""
9279
+ session_id = task.session_id
9250
9280
  is_real = bool(session_id) and not session_id.startswith("__new__-")
9251
- lineage_ids = (
9252
- self._codex_lineage_ids(session_id)
9253
- if is_real and session_id is not None else set()
9254
- )
9255
9281
  deleted_ids: set[str] = set()
9256
9282
  failed_ids: set[str] = set()
9257
- if is_real:
9283
+ if is_real and session_id is not None:
9258
9284
  # A collapsed row represents the complete rewind lineage. Delete
9259
9285
  # every provider UUID so an older checkpoint cannot immediately
9260
9286
  # reappear as a "new" session after the newest leaf is removed.
9287
+ lineage_ids = set(task.lineage_ids) or {session_id}
9261
9288
  ordered = [session_id] + sorted(lineage_ids - {session_id})
9262
9289
  for candidate in ordered:
9263
9290
  if self._codex_delete(candidate):
@@ -9265,29 +9292,145 @@ class App:
9265
9292
  else:
9266
9293
  failed_ids.add(candidate)
9267
9294
  if failed_ids and not deleted_ids:
9268
- self._set_status(f"codex delete failed: {label}", "error")
9269
- return
9270
- self._forget_running(session_id, tmux_name)
9271
- if isinstance(self._codex_index, BackgroundCodexIndex):
9272
- self._codex_index.invalidate(
9273
- tombstones=deleted_ids)
9274
- else:
9275
- self._codex_index.invalidate()
9276
- self._invalidate_project_snapshot()
9277
- self._refresh()
9295
+ return _DeleteResult(
9296
+ task=task,
9297
+ status_text=f"codex delete failed: {task.label}",
9298
+ status_level="error",
9299
+ views_changed=task.pane_identity is not None,
9300
+ )
9278
9301
  if failed_ids:
9279
- self._set_status(
9280
- f"Deleted {len(deleted_ids)}/{len(lineage_ids)} rewind "
9281
- f"records for {label}; {len(failed_ids)} failed",
9282
- "error",
9302
+ return _DeleteResult(
9303
+ task=task,
9304
+ status_text=(
9305
+ f"Deleted {len(deleted_ids)}/{len(task.lineage_ids)} "
9306
+ f"rewind records for {task.label}; "
9307
+ f"{len(failed_ids)} failed"
9308
+ ),
9309
+ status_level="error",
9310
+ remove_running=True,
9311
+ views_changed=True,
9312
+ invalidate_codex=True,
9313
+ deleted_codex_ids=frozenset(deleted_ids),
9283
9314
  )
9284
- return
9285
9315
  suffix = (
9286
9316
  f" ({len(deleted_ids)} rewind records)"
9287
9317
  if len(deleted_ids) > 1 else ""
9288
9318
  )
9289
- self._set_status(
9290
- f"{'Deleted' if is_real else 'Killed'}: {label}{suffix}")
9319
+ return _DeleteResult(
9320
+ task=task,
9321
+ status_text=(
9322
+ f"{'Deleted' if is_real else 'Killed'}: {task.label}{suffix}"
9323
+ ),
9324
+ remove_running=True,
9325
+ views_changed=True,
9326
+ invalidate_codex=True,
9327
+ deleted_codex_ids=frozenset(deleted_ids),
9328
+ )
9329
+
9330
+ def _execute_claude_delete(self, task: _DeleteTask) -> _DeleteResult:
9331
+ """Delete Claude history and metadata in the worker."""
9332
+ session_id = task.session_id
9333
+ jsonl_path = task.jsonl_path
9334
+ path_changed = False
9335
+ if jsonl_path is not None:
9336
+ try:
9337
+ jsonl_path.unlink(missing_ok=True)
9338
+ path_changed = True
9339
+ except OSError as exc:
9340
+ return _DeleteResult(
9341
+ task=task,
9342
+ status_text=f"failed to delete {jsonl_path}: {exc}",
9343
+ status_level="error",
9344
+ remove_running=True,
9345
+ views_changed=True,
9346
+ )
9347
+
9348
+ is_real = bool(session_id) and not session_id.startswith("__new__-")
9349
+ if is_real and session_id is not None:
9350
+ env_dir = task.claude_home / "session-env" / session_id
9351
+ if env_dir.is_dir():
9352
+ shutil.rmtree(env_dir, ignore_errors=True)
9353
+ history_ok = self._remove_from_history(
9354
+ session_id, claude_home=task.claude_home) is not False
9355
+ else:
9356
+ history_ok = True
9357
+
9358
+ # A writer outside the captured tmux process tree could race the first
9359
+ # unlink. Verify once after history cleanup; a surviving file means the
9360
+ # requested delete did not complete and must not be reported as such.
9361
+ if jsonl_path is not None and jsonl_path.exists():
9362
+ try:
9363
+ jsonl_path.unlink(missing_ok=True)
9364
+ path_changed = True
9365
+ except OSError as exc:
9366
+ return _DeleteResult(
9367
+ task=task,
9368
+ status_text=("session was recreated and could not be "
9369
+ f"deleted: {exc}"),
9370
+ status_level="error",
9371
+ remove_running=True,
9372
+ views_changed=True,
9373
+ claude_path_changed=path_changed,
9374
+ )
9375
+
9376
+ deleted = is_real and jsonl_path is not None
9377
+ if not history_ok:
9378
+ status_text = (
9379
+ f"Deleted: {task.label} (history index cleanup failed)"
9380
+ )
9381
+ status_level = "warn"
9382
+ else:
9383
+ status_text = f"{'Deleted' if deleted else 'Killed'}: {task.label}"
9384
+ status_level = "info"
9385
+ return _DeleteResult(
9386
+ task=task,
9387
+ status_text=status_text,
9388
+ status_level=status_level,
9389
+ remove_running=True,
9390
+ views_changed=True,
9391
+ claude_path_changed=path_changed,
9392
+ )
9393
+
9394
+ def _delete_pending(self) -> bool:
9395
+ """Whether a cleanup is running or waiting for UI-thread publication."""
9396
+ lock = getattr(self, "_delete_lock", None)
9397
+ if lock is None:
9398
+ return False
9399
+ with lock:
9400
+ thread = getattr(self, "_delete_thread", None)
9401
+ return bool(
9402
+ getattr(self, "_delete_result", None) is not None
9403
+ or thread is not None and thread.is_alive()
9404
+ )
9405
+
9406
+ def _consume_delete_result(self) -> bool:
9407
+ """Publish completed destructive work on the Urwid thread."""
9408
+ lock = getattr(self, "_delete_lock", None)
9409
+ if lock is None:
9410
+ return False
9411
+ with lock:
9412
+ result = getattr(self, "_delete_result", None)
9413
+ if result is None:
9414
+ return False
9415
+ self._delete_result = None
9416
+ self._delete_thread = None
9417
+
9418
+ task = result.task
9419
+ if result.remove_running:
9420
+ self._forget_running(task.session_id, task.tmux_name)
9421
+ if result.claude_path_changed and task.jsonl_path is not None:
9422
+ invalidate_session(task.jsonl_path)
9423
+ if result.invalidate_codex:
9424
+ if isinstance(self._codex_index, BackgroundCodexIndex):
9425
+ self._codex_index.invalidate(
9426
+ tombstones=set(result.deleted_codex_ids))
9427
+ else:
9428
+ self._codex_index.invalidate()
9429
+ if result.views_changed:
9430
+ self._session_cache.invalidate()
9431
+ self._invalidate_project_snapshot()
9432
+ self._set_status(result.status_text, result.status_level, force=True)
9433
+ return True
9291
9434
 
9292
9435
  def _codex_delete(self, uuid: str) -> bool:
9293
9436
  """Run ``codex delete --force <UUID>`` against the resolved CODEX_HOME.
@@ -10342,6 +10485,15 @@ class App:
10342
10485
  ``set_message`` that clobbered one-shot messages every poll.
10343
10486
  """
10344
10487
  now = time.monotonic()
10488
+ if self._delete_pending():
10489
+ # This is operation state, not a one-shot acknowledgement. Keep it
10490
+ # alive past the ordinary info TTL. A newly raised warning/error
10491
+ # retains its short severity hold, after which progress returns.
10492
+ if self._status_text != "Deleting…":
10493
+ self._set_status("Deleting…", "info")
10494
+ if self._status_text == "Deleting…":
10495
+ self._status_since = now
10496
+ return
10345
10497
  if self._status_text is not None:
10346
10498
  ttl = self._STATUS_TTL.get(self._status_level, 6.0)
10347
10499
  if ttl is None or now - self._status_since < ttl:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.3.dev4
3
+ Version: 0.3.3.dev5
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
@@ -204,7 +204,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
204
204
  | `r` | Rename the focused session |
205
205
  | `s` | Toggle star — starred sessions pinned to top with ⭐ |
206
206
  | `k` | Kill the running agent process (keeps session file) |
207
- | `d` | Delete the focused session (prompts for confirmation) |
207
+ | `d` | Delete the focused session (confirms, then finishes in background) |
208
208
  | `t` | Open or return to the managed terminal below the Target agent |
209
209
  | `T` | Return to that agent's managed Vim viewer, when open |
210
210
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
@@ -242,6 +242,10 @@ The rename popup starts with the current title pre-filled. Press
242
242
  `Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
243
243
  to cancel.
244
244
 
245
+ After a delete is confirmed, its popup closes immediately. The bottom-right
246
+ status shows **Deleting…** while Railmux stops the exact provider session and
247
+ removes its history in the background, then reports the final result.
248
+
245
249
  Press `/` in Projects, Sessions, or Running to filter that section. Active
246
250
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
247
251
  shows the current value. Press `Ctrl-U` in the filter editor to clear it, then