mixhub-cli 0.2.1__tar.gz → 0.2.2__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 (266) hide show
  1. mixhub_cli-0.2.2/.github/assets/terminal.png +0 -0
  2. mixhub_cli-0.2.2/.github/assets/web-dark.png +0 -0
  3. mixhub_cli-0.2.2/.github/assets/web-light.png +0 -0
  4. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/PKG-INFO +16 -14
  5. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/README.md +16 -14
  6. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/__init__.py +1 -1
  7. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/main.py +3 -1
  8. mixhub_cli-0.2.2/mixhub/core/cli_version.py +75 -0
  9. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/discovery.py +42 -11
  10. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/send.py +3 -1
  11. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/session_filters.py +49 -33
  12. mixhub_cli-0.2.2/mixhub/hub/shell.py +85 -0
  13. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/summary.py +9 -4
  14. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/ops/doctor.py +37 -0
  15. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/classify.py +2 -1
  16. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/patterns.py +4 -0
  17. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/quota.py +7 -2
  18. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/statusline.py +16 -8
  19. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/headers.py +1 -1
  20. mixhub_cli-0.2.2/mixhub/tmux/home_shell.py +141 -0
  21. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/new_sessions.py +10 -3
  22. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/onexit.py +48 -38
  23. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/pool.py +2 -1
  24. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/session_state.py +19 -0
  25. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/workspace.py +2 -2
  26. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/badges.py +2 -2
  27. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/style.py +6 -6
  28. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/drafts.py +35 -2
  29. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/actions.py +27 -8
  30. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/colors.py +6 -6
  31. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/draw.py +7 -1
  32. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/loop.py +1 -0
  33. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/selection_sync.py +25 -1
  34. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/sidebar.py +28 -11
  35. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/quota_table.py +1 -1
  36. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/static/app.js +2 -2
  37. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/static/index.html +2 -2
  38. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/static/style.css +4 -4
  39. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/server.py +1 -1
  40. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_headless_and_hibernate_state.py +14 -0
  41. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_e2e_ux_audit.py +26 -12
  42. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_hub.py +122 -2
  43. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_probe.py +26 -0
  44. mixhub_cli-0.2.2/tests/test_session_hygiene.py +316 -0
  45. mixhub_cli-0.2.2/tests/test_sidebar_pinned_order.py +22 -0
  46. mixhub_cli-0.2.1/.github/assets/web-dark.png +0 -0
  47. mixhub_cli-0.2.1/.github/assets/web-light.png +0 -0
  48. mixhub_cli-0.2.1/mixhub/hub/shell.py +0 -49
  49. mixhub_cli-0.2.1/mixhub/tmux/home_shell.py +0 -62
  50. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/.github/workflows/publish.yml +0 -0
  51. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/.github/workflows/test.yml +0 -0
  52. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/.gitignore +0 -0
  53. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/LICENSE +0 -0
  54. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/Preferences.example +0 -0
  55. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/bin/hub +0 -0
  56. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/bin/mixhub +0 -0
  57. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/install.sh +0 -0
  58. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/__main__.py +0 -0
  59. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/__init__.py +0 -0
  60. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/base.py +0 -0
  61. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/cli.py +0 -0
  62. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/config.py +0 -0
  63. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/hooks.py +0 -0
  64. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/manager.py +0 -0
  65. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/slack.py +0 -0
  66. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/telegram.py +0 -0
  67. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/text.py +0 -0
  68. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/webhook.py +0 -0
  69. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/__init__.py +0 -0
  70. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/daemon.py +0 -0
  71. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/sessions.py +0 -0
  72. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/tools.py +0 -0
  73. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/update.py +0 -0
  74. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/workspace.py +0 -0
  75. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/__init__.py +0 -0
  76. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/browser.py +0 -0
  77. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/env.py +0 -0
  78. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/launch.py +0 -0
  79. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/paths.py +0 -0
  80. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/prefs.py +0 -0
  81. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/state.py +0 -0
  82. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/textwidth.py +0 -0
  83. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/__init__.py +0 -0
  84. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/__init__.py +0 -0
  85. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/__main__.py +0 -0
  86. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/cursor.py +0 -0
  87. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/debuglog.py +0 -0
  88. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/main.py +0 -0
  89. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/mouse.py +0 -0
  90. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/patterns.py +0 -0
  91. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/prefs.py +0 -0
  92. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/stream.py +0 -0
  93. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/vscreen.py +0 -0
  94. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/__init__.py +0 -0
  95. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/bundle.py +0 -0
  96. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/contract.py +0 -0
  97. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/disposition.py +0 -0
  98. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/extract.py +0 -0
  99. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/relay.py +0 -0
  100. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/resume.py +0 -0
  101. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/text.py +0 -0
  102. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/__init__.py +0 -0
  103. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/actions.py +0 -0
  104. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/constants.py +0 -0
  105. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/hub.py +0 -0
  106. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/index.py +0 -0
  107. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/integrations.py +0 -0
  108. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/lifecycle.py +0 -0
  109. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/listing.py +0 -0
  110. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/metrics.py +0 -0
  111. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/opencode.py +0 -0
  112. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/pane_probe.py +0 -0
  113. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/processes.py +0 -0
  114. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/schema.py +0 -0
  115. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/sleep_blockers.py +0 -0
  116. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/text.py +0 -0
  117. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/titles.py +0 -0
  118. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/transcript.py +0 -0
  119. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/util.py +0 -0
  120. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/working_set.py +0 -0
  121. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/__init__.py +0 -0
  122. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/en.json +0 -0
  123. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/ja.json +0 -0
  124. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/zh.json +0 -0
  125. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/ops/__init__.py +0 -0
  126. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/__init__.py +0 -0
  127. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/capture.py +0 -0
  128. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/menus.py +0 -0
  129. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/rules.py +0 -0
  130. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/text.py +0 -0
  131. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/__init__.py +0 -0
  132. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/classify.py +0 -0
  133. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/guard.py +0 -0
  134. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/identity.py +0 -0
  135. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/tree.py +0 -0
  136. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/__init__.py +0 -0
  137. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/claude.py +0 -0
  138. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/deepseek.py +0 -0
  139. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/jsonl.py +0 -0
  140. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/launch.py +0 -0
  141. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/native_titles.py +0 -0
  142. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/preferences.py +0 -0
  143. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/presets.py +0 -0
  144. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/readiness.py +0 -0
  145. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/resume.py +0 -0
  146. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/title_overrides.py +0 -0
  147. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/__init__.py +0 -0
  148. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/blocks.py +0 -0
  149. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/sync.py +0 -0
  150. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/trust.py +0 -0
  151. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/__init__.py +0 -0
  152. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/commands.py +0 -0
  153. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/gateway.py +0 -0
  154. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/outer.py +0 -0
  155. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/__init__.py +0 -0
  156. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/__main__.py +0 -0
  157. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/app.py +0 -0
  158. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/caffeinate.py +0 -0
  159. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/__init__.py +0 -0
  160. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/canvas.py +0 -0
  161. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/events.py +0 -0
  162. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/geometry.py +0 -0
  163. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/hitmap.py +0 -0
  164. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/input.py +0 -0
  165. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/loop.py +0 -0
  166. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/widget.py +0 -0
  167. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/diag.py +0 -0
  168. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/__init__.py +0 -0
  169. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/attention.py +0 -0
  170. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/evq.py +0 -0
  171. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/footer.py +0 -0
  172. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/header.py +0 -0
  173. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/home.py +0 -0
  174. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/layout.py +0 -0
  175. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/mouse.py +0 -0
  176. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/navigation.py +0 -0
  177. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/popovers.py +0 -0
  178. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/prompt.py +0 -0
  179. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/reader.py +0 -0
  180. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/refresh.py +0 -0
  181. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/relay.py +0 -0
  182. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/wordmark_row.py +0 -0
  183. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/paths.py +0 -0
  184. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/quota.py +0 -0
  185. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/selection.py +0 -0
  186. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/settings.py +0 -0
  187. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/sidebar_rows.py +0 -0
  188. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/web.py +0 -0
  189. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/__init__.py +0 -0
  190. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/chip.py +0 -0
  191. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/divider.py +0 -0
  192. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/header_bar.py +0 -0
  193. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/popover.py +0 -0
  194. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/preview_bar.py +0 -0
  195. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/sidebar_row.py +0 -0
  196. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/toast.py +0 -0
  197. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/wordmark.py +0 -0
  198. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/__init__.py +0 -0
  199. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/__main__.py +0 -0
  200. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/handler.py +0 -0
  201. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/server.py +0 -0
  202. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/pyproject.toml +0 -0
  203. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/scripts/demo_seed.py +0 -0
  204. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/browser_check.mjs +0 -0
  205. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/README.md +0 -0
  206. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/agy_claude_filter.py +0 -0
  207. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/channels.py +0 -0
  208. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/handoff.py +0 -0
  209. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/mixhub_cli.py +0 -0
  210. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/probe.py +0 -0
  211. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/proc_guard.py +0 -0
  212. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/providers.py +0 -0
  213. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/rules_sync.py +0 -0
  214. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/tui.py +0 -0
  215. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/wordmark.py +0 -0
  216. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/conftest.py +0 -0
  217. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/fixtures/agy_1_4_7_altscreen_typed_120x40.bin +0 -0
  218. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/fixtures/agy_1_4_7_altscreen_wrap_paste_40x30.bin +0 -0
  219. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/mcp_client.mjs +0 -0
  220. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_filter_mouse.py +0 -0
  221. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_filter_package.py +0 -0
  222. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_headless_continued.py +0 -0
  223. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_auto_sleep_pref.py +0 -0
  224. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_bg_proc_scan.py +0 -0
  225. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_caffeinate.py +0 -0
  226. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_channels_config.py +0 -0
  227. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_consumer_checks.py +0 -0
  228. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_demo_seed.py +0 -0
  229. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_env_names.py +0 -0
  230. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_first_run.py +0 -0
  231. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_handoff_hold_and_fork.py +0 -0
  232. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_harness_presets.py +0 -0
  233. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_hub_providers_layout.py +0 -0
  234. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_hubpool_duplicate_windows.py +0 -0
  235. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_i18n.py +0 -0
  236. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_launch_forms.py +0 -0
  237. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_modular_and_trust.py +0 -0
  238. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_modular_channels_handoff.py +0 -0
  239. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_no_agent_file_deletion.py +0 -0
  240. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_notify_hooks.py +0 -0
  241. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_opencode.py +0 -0
  242. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_oss_desensitize_and_cc_ds.py +0 -0
  243. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_own_tmux_server.py +0 -0
  244. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_package_import_rule.py +0 -0
  245. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_packaging.py +0 -0
  246. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_platform_support.py +0 -0
  247. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_preferences_lookup.py +0 -0
  248. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_proc_guard.py +0 -0
  249. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_providers_live.py +0 -0
  250. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_rules_sync.py +0 -0
  251. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_schema_resilience.py +0 -0
  252. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_server.py +0 -0
  253. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_state_root.py +0 -0
  254. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_statusline_command.py +0 -0
  255. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_subagent_filter.py +0 -0
  256. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_telegram_bot_api_send.py +0 -0
  257. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_test_isolation.py +0 -0
  258. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_tui_core_primitives.py +0 -0
  259. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_tui_module_layout.py +0 -0
  260. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_update.py +0 -0
  261. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_web_navigation.py +0 -0
  262. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_windows_compat.py +0 -0
  263. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_wordmark.py +0 -0
  264. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_working_set_removal_log.py +0 -0
  265. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_zoom_toggle_and_badge.py +0 -0
  266. {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/webui_composer.test.mjs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mixhub-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A terminal sidebar for your Claude Code, Codex, Gemini and OpenCode sessions.
5
5
  Project-URL: Homepage, https://github.com/ShinoKana/mixhub
6
6
  Project-URL: Repository, https://github.com/ShinoKana/mixhub.git
@@ -64,17 +64,19 @@ What it doesn't do: it doesn't create worktrees or touch git, doesn't patch the
64
64
 
65
65
  ## Similar tools
66
66
 
67
- | | Agents | Runs in | Git worktrees | Per-session status | Sessions started elsewhere |
68
- | :--- | :--- | :--- | :--- | :--- | :--- |
69
- | MixHub | Claude Code, Codex, Gemini, OpenCode | any terminal (needs tmux) | no | from session files | listed |
70
- | [Claude Code Agent View](https://code.claude.com/docs/en/agent-view) | Claude Code | the `claude` CLI | background sessions by default | yes | only after `/bg` or `claude --bg` |
71
- | [cmux](https://github.com/manaflow-ai/cmux) | Claude Code, Codex, OpenCode, Gemini, Amp and more | its own macOS app | no | notifications via hooks / OSC | not documented |
72
- | [claude-squad](https://github.com/smtg-ai/claude-squad) | Claude Code, Codex, Gemini, Aider | any terminal (needs tmux) | one per session | not documented | no, manages its own |
73
- | [ccmanager](https://github.com/kbwo/ccmanager) | Claude Code, Codex, Gemini, OpenCode and more | any terminal | yes, managed in the app | busy / waiting / idle | not documented |
74
- | [agent-deck](https://github.com/asheshgoplani/agent-deck) | Claude Code, Codex, Gemini, OpenCode and more | any terminal (tmux) | optional | running / waiting / idle | its own tmux sessions |
75
- | [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) | Claude Code, Codex, Devin | inside your tmux | no | via agent hooks | inside tmux |
67
+ | | Other agents | Runs in | Status | Outside sessions |
68
+ | :--- | :--- | :--- | :--- | :--- |
69
+ | MixHub | Codex, Gemini, OpenCode | tmux | session files | listed |
70
+ | [Agent View](https://code.claude.com/docs/en/agent-view) | none | `claude` CLI | yes | after `/bg` |
71
+ | [cmux](https://github.com/manaflow-ai/cmux) | Codex, Gemini, OpenCode, … | macOS app | hooks / OSC | – |
72
+ | [claude-squad](https://github.com/smtg-ai/claude-squad) | Codex, Gemini, Aider | tmux | – | no |
73
+ | [ccmanager](https://github.com/kbwo/ccmanager) | Codex, Gemini, OpenCode, … | any terminal | yes | – |
74
+ | [agent-deck](https://github.com/asheshgoplani/agent-deck) | Codex, Gemini, OpenCode, … | tmux | yes | no |
75
+ | [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) | Codex, Devin | your tmux | hooks | if in tmux |
76
76
 
77
- If you only use Claude Code, Agent View is built in. If you want a worktree per task, claude-squad and ccmanager do that and MixHub doesn't.
77
+ – means the project's docs don't say.
78
+
79
+ If you only use Claude Code, Agent View is built in. If you want a git worktree per task, claude-squad and ccmanager make one for you (agent-deck optionally); MixHub doesn't touch git.
78
80
 
79
81
  ## Install
80
82
 
@@ -123,7 +125,7 @@ mixhub update --check # only say whether a newer version exists
123
125
  | Codex | `~/.codex/sessions/` rollouts + state DB | `codex resume <id>` |
124
126
  | OpenCode | `opencode.db` (read-only) | `opencode -s <id>` |
125
127
 
126
- `✎ New session` can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
128
+ `✎ New session` opens the new session in the working directory of the session selected in the sidebar, so to start in a project folder, click one of that folder's sessions first. With nothing selected, including the `LAUNCH` buttons on Home, it uses the folder you ran `mixhub` from. It can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
127
129
 
128
130
  ## The workspace
129
131
 
@@ -131,7 +133,7 @@ mixhub update --check # only say whether a newer version exists
131
133
 
132
134
  **Right pane.** Clicking a session that is running in MixHub switches to its live CLI window. Clicking any other session shows a read-only transcript preview with a turn minimap; `[ Resume ↵ ]` starts the CLI in place, and `[ Go to Tab ]` jumps to a session that is running in another Terminal tab. Each live window has a two-row header with the working directory, session id and title.
133
135
 
134
- **Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L).
136
+ **Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L). An agent started there (`claude`, `codex`, `opencode`, `agy`) moves to its own window in `Active`, the same as `✎ New session`, and Home gets a fresh shell; one-shot runs like `claude -p` stay in the shell.
135
137
 
136
138
  **Footer.** Used `5h` and weekly quota with reset times for each agent that has at least one session on this machine, plus CPU and memory. Codex quota comes from its own session files. For exact Claude Code and Gemini (AGY) numbers, point their status line at MixHub, which records the quota each agent reports and prints a short status line back:
137
139
 
@@ -265,7 +267,7 @@ Everything works without configuration. Optional settings live in a JSON file na
265
267
  | `MIXHUB_NO_OPEN=1` | `mixhub web` starts the daemon without opening a browser |
266
268
  | `MIXHUB_NO_UPDATE_CHECK=1` | No daily update check |
267
269
 
268
- `host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones. Prompts that your own automation sends can be hidden from the list with `data/local_rules.json` (`automated_prefixes`, `automated_substrings`). Notes your own tools type into an agent on another agent's behalf go in `relayed_prefixes`: they never count as you having used a session, but don't hide one either.
270
+ `host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones. Sessions another agent started are hidden by what their CLI records (subagents, headless runs, Codex tasks whose `originator` is Claude Code). For older sessions without such a record, prompts that your own automation sends can be hidden with `data/local_rules.json` (`automated_prefixes`, `automated_substrings`, and `codex_automated_prefixes` for Codex only); `mixhub doctor --hidden` lists every session a text rule hides. Notes your own tools type into an agent on another agent's behalf go in `relayed_prefixes`: they never count as you having used a session, but don't hide one either.
269
271
 
270
272
  ## Architecture
271
273
 
@@ -37,17 +37,19 @@ What it doesn't do: it doesn't create worktrees or touch git, doesn't patch the
37
37
 
38
38
  ## Similar tools
39
39
 
40
- | | Agents | Runs in | Git worktrees | Per-session status | Sessions started elsewhere |
41
- | :--- | :--- | :--- | :--- | :--- | :--- |
42
- | MixHub | Claude Code, Codex, Gemini, OpenCode | any terminal (needs tmux) | no | from session files | listed |
43
- | [Claude Code Agent View](https://code.claude.com/docs/en/agent-view) | Claude Code | the `claude` CLI | background sessions by default | yes | only after `/bg` or `claude --bg` |
44
- | [cmux](https://github.com/manaflow-ai/cmux) | Claude Code, Codex, OpenCode, Gemini, Amp and more | its own macOS app | no | notifications via hooks / OSC | not documented |
45
- | [claude-squad](https://github.com/smtg-ai/claude-squad) | Claude Code, Codex, Gemini, Aider | any terminal (needs tmux) | one per session | not documented | no, manages its own |
46
- | [ccmanager](https://github.com/kbwo/ccmanager) | Claude Code, Codex, Gemini, OpenCode and more | any terminal | yes, managed in the app | busy / waiting / idle | not documented |
47
- | [agent-deck](https://github.com/asheshgoplani/agent-deck) | Claude Code, Codex, Gemini, OpenCode and more | any terminal (tmux) | optional | running / waiting / idle | its own tmux sessions |
48
- | [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) | Claude Code, Codex, Devin | inside your tmux | no | via agent hooks | inside tmux |
49
-
50
- If you only use Claude Code, Agent View is built in. If you want a worktree per task, claude-squad and ccmanager do that and MixHub doesn't.
40
+ | | Other agents | Runs in | Status | Outside sessions |
41
+ | :--- | :--- | :--- | :--- | :--- |
42
+ | MixHub | Codex, Gemini, OpenCode | tmux | session files | listed |
43
+ | [Agent View](https://code.claude.com/docs/en/agent-view) | none | `claude` CLI | yes | after `/bg` |
44
+ | [cmux](https://github.com/manaflow-ai/cmux) | Codex, Gemini, OpenCode, … | macOS app | hooks / OSC | – |
45
+ | [claude-squad](https://github.com/smtg-ai/claude-squad) | Codex, Gemini, Aider | tmux | – | no |
46
+ | [ccmanager](https://github.com/kbwo/ccmanager) | Codex, Gemini, OpenCode, … | any terminal | yes | – |
47
+ | [agent-deck](https://github.com/asheshgoplani/agent-deck) | Codex, Gemini, OpenCode, … | tmux | yes | no |
48
+ | [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) | Codex, Devin | your tmux | hooks | if in tmux |
49
+
50
+ – means the project's docs don't say.
51
+
52
+ If you only use Claude Code, Agent View is built in. If you want a git worktree per task, claude-squad and ccmanager make one for you (agent-deck optionally); MixHub doesn't touch git.
51
53
 
52
54
  ## Install
53
55
 
@@ -96,7 +98,7 @@ mixhub update --check # only say whether a newer version exists
96
98
  | Codex | `~/.codex/sessions/` rollouts + state DB | `codex resume <id>` |
97
99
  | OpenCode | `opencode.db` (read-only) | `opencode -s <id>` |
98
100
 
99
- `✎ New session` can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
101
+ `✎ New session` opens the new session in the working directory of the session selected in the sidebar, so to start in a project folder, click one of that folder's sessions first. With nothing selected, including the `LAUNCH` buttons on Home, it uses the folder you ran `mixhub` from. It can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
100
102
 
101
103
  ## The workspace
102
104
 
@@ -104,7 +106,7 @@ mixhub update --check # only say whether a newer version exists
104
106
 
105
107
  **Right pane.** Clicking a session that is running in MixHub switches to its live CLI window. Clicking any other session shows a read-only transcript preview with a turn minimap; `[ Resume ↵ ]` starts the CLI in place, and `[ Go to Tab ]` jumps to a session that is running in another Terminal tab. Each live window has a two-row header with the working directory, session id and title.
106
108
 
107
- **Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L).
109
+ **Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L). An agent started there (`claude`, `codex`, `opencode`, `agy`) moves to its own window in `Active`, the same as `✎ New session`, and Home gets a fresh shell; one-shot runs like `claude -p` stay in the shell.
108
110
 
109
111
  **Footer.** Used `5h` and weekly quota with reset times for each agent that has at least one session on this machine, plus CPU and memory. Codex quota comes from its own session files. For exact Claude Code and Gemini (AGY) numbers, point their status line at MixHub, which records the quota each agent reports and prints a short status line back:
110
112
 
@@ -238,7 +240,7 @@ Everything works without configuration. Optional settings live in a JSON file na
238
240
  | `MIXHUB_NO_OPEN=1` | `mixhub web` starts the daemon without opening a browser |
239
241
  | `MIXHUB_NO_UPDATE_CHECK=1` | No daily update check |
240
242
 
241
- `host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones. Prompts that your own automation sends can be hidden from the list with `data/local_rules.json` (`automated_prefixes`, `automated_substrings`). Notes your own tools type into an agent on another agent's behalf go in `relayed_prefixes`: they never count as you having used a session, but don't hide one either.
243
+ `host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones. Sessions another agent started are hidden by what their CLI records (subagents, headless runs, Codex tasks whose `originator` is Claude Code). For older sessions without such a record, prompts that your own automation sends can be hidden with `data/local_rules.json` (`automated_prefixes`, `automated_substrings`, and `codex_automated_prefixes` for Codex only); `mixhub doctor --hidden` lists every session a text rule hides. Notes your own tools type into an agent on another agent's behalf go in `relayed_prefixes`: they never count as you having used a session, but don't hide one either.
242
244
 
243
245
  ## Architecture
244
246
 
@@ -7,6 +7,6 @@ The README's Architecture section maps the subpackages (`hub`, `providers`, `tui
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
- __version__ = "0.2.1"
10
+ __version__ = "0.2.2"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -30,7 +30,9 @@ USAGE = """Usage: mixhub [command]
30
30
  mixhub web open the web workbench (starts the local daemon)
31
31
  mixhub start|stop|status
32
32
  control the web daemon without opening a browser
33
- mixhub doctor check agents, tmux, channels and terminal settings
33
+ mixhub doctor [--hidden]
34
+ check agents, tmux, channels and terminal settings;
35
+ --hidden lists sessions hidden only by a text rule
34
36
  mixhub handoff SESSION_ID --to claude|codex|agy [--mode delegate|review] [--dry-run]
35
37
  hand a session over to another agent
36
38
  mixhub notify ... send a notification through the configured channels
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """`mixhub.core.cli_version`: Installed Claude Code version against the version a live session started with.
3
+
4
+ Only a hint: a session on an older build keeps running it until it is restarted, and MixHub never restarts one
5
+ itself (a restart drops Remote Control, and Claude Code refuses `/restart` after a `cd`).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import glob
10
+ import json
11
+ import os
12
+ import re
13
+ import shutil
14
+ import time
15
+
16
+ _VERSION_RE = re.compile(r"^\d+(\.\d+)+$")
17
+ _CACHE_SEC = 30.0
18
+ _installed_cache: dict[str, tuple[float, str]] = {}
19
+ _running_cache: dict[str, tuple[float, dict[str, str]]] = {}
20
+
21
+
22
+ def installed_claude_version() -> str:
23
+ """Version of the `claude` on PATH, read from the native installer's `versions/<x.y.z>` symlink target.
24
+ Empty when it can't be told without running the binary (npm installs, wrappers)."""
25
+ now = time.monotonic()
26
+ hit = _installed_cache.get("claude")
27
+ if hit and now - hit[0] < _CACHE_SEC:
28
+ return hit[1]
29
+ ver = ""
30
+ path = shutil.which("claude")
31
+ if path:
32
+ name = os.path.basename(os.path.realpath(path))
33
+ if _VERSION_RE.match(name):
34
+ ver = name
35
+ _installed_cache["claude"] = (now, ver)
36
+ return ver
37
+
38
+
39
+ def running_claude_versions() -> dict[str, str]:
40
+ """`{sessionId: version}` from the live processes' `~/.claude/sessions/<pid>.json`, for a `--header` pane that
41
+ has no discovery of its own."""
42
+ now = time.monotonic()
43
+ hit = _running_cache.get("claude")
44
+ if hit and now - hit[0] < _CACHE_SEC:
45
+ return hit[1]
46
+ found: dict[str, str] = {}
47
+ for fp in glob.glob(os.path.join(os.path.expanduser("~"), ".claude", "sessions", "*.json")):
48
+ try:
49
+ with open(fp, encoding="utf-8") as f:
50
+ info = json.load(f)
51
+ pid, sid, ver = int(info["pid"]), str(info["sessionId"]), str(info.get("version") or "")
52
+ os.kill(pid, 0)
53
+ except (OSError, ValueError, KeyError, TypeError):
54
+ continue
55
+ if ver:
56
+ found[sid] = ver
57
+ _running_cache["claude"] = (now, found)
58
+ return found
59
+
60
+
61
+ def _as_tuple(ver: str) -> tuple[int, ...]:
62
+ return tuple(int(x) for x in ver.split("."))
63
+
64
+
65
+ def old_version_hint(s: dict) -> str:
66
+ """`old 2.1.316 · 2.1.318 installed` for a Claude session started on an older build, else empty."""
67
+ if not s or s.get("tool") != "claude":
68
+ return ""
69
+ running = str(s.get("cliVersion") or running_claude_versions().get(str(s.get("id") or ""), ""))
70
+ if not _VERSION_RE.match(running):
71
+ return ""
72
+ installed = installed_claude_version()
73
+ if not installed or _as_tuple(running) >= _as_tuple(installed):
74
+ return ""
75
+ return f"old {running} · {installed} installed"
@@ -31,9 +31,9 @@ from mixhub.hub.schema import claude_is_headless_marker, codex_state_db_candidat
31
31
  from mixhub.hub.session_filters import (
32
32
  _load_local_rules,
33
33
  _LOCAL_RULES_CACHE,
34
+ codex_automation_reason,
34
35
  is_automated_or_subagent_text,
35
36
  is_users_own_prompt,
36
- is_codex_automated,
37
37
  is_temp_session,
38
38
  )
39
39
  from mixhub.hub.shell import resume_command
@@ -46,6 +46,21 @@ from mixhub.proc.identity import get_boot_id
46
46
  from mixhub.proc.tree import has_agent_ancestor
47
47
 
48
48
 
49
+ def settle_parked_status(value: dict, jobs_dir: str) -> dict:
50
+ """
51
+ A Claude session that parked its turn into a background job (`parkedJobId`) keeps the `busy` it had at that
52
+ moment in its pid file. The work runs in the job, so the session is busy only while the job is still working.
53
+ """
54
+ job_id = str(value.get("parkedJobId") or "")
55
+ if job_id and value.get("status") == "busy":
56
+ job = read_json(os.path.join(jobs_dir, job_id, "state.json"), {})
57
+ if not isinstance(job, dict) or job.get("state") != "working":
58
+ value["status"] = "idle"
59
+ # Its transcript also stops mid-turn at the hand-off, which would otherwise still read as working
60
+ value["_parkedDone"] = True
61
+ return value
62
+
63
+
49
64
  class DiscoveryMixin:
50
65
  """Discover sessions from every provider store and merge them with live processes and hub state."""
51
66
 
@@ -298,7 +313,7 @@ class DiscoveryMixin:
298
313
  value["_mtime"] = os.stat(path).st_mtime
299
314
  except OSError:
300
315
  value["_mtime"] = 0
301
- pid_states[int(value["pid"])] = value
316
+ pid_states[int(value["pid"])] = settle_parked_status(value, self.path(".claude/jobs"))
302
317
  self.sessions.close()
303
318
  self.sessions = sessions = SessionIndex()
304
319
 
@@ -548,7 +563,7 @@ class DiscoveryMixin:
548
563
  # archived_sessions/ only resolves rows whose recorded rollout_path moved; it never adds new rows.
549
564
  codex_session_files, codex_disk_paths = self._discover_codex_rollouts()
550
565
  codex_disk_paths_set = set(codex_session_files) | set(codex_disk_paths.values())
551
- codex_columns = ["id", "title", "cwd", "first_user_message", "updated_at", "created_at", "model", "reasoning_effort", "rollout_path", "agent_role", "source"]
566
+ codex_columns = ["id", "title", "cwd", "first_user_message", "updated_at", "created_at", "model", "reasoning_effort", "rollout_path", "agent_role", "source", "originator"]
552
567
  known_codex_paths = set()
553
568
  # Follow Codex's state DB schema bumps (state_5 -> state_6 ...): newest file that actually
554
569
  # has a `threads` table wins; otherwise fall back to older candidates.
@@ -572,18 +587,20 @@ class DiscoveryMixin:
572
587
  len(raw_first),
573
588
  raw_first[:128],
574
589
  csrc,
590
+ row.get("originator"),
575
591
  row.get("agent_role"),
576
592
  rules_mtime,
577
593
  )
578
594
  cmemo = self._codex_row_memo.get(rid)
579
595
  if cmemo is not None and cmemo[0] == codex_sig:
580
- parsed_title, parsed_first, row_is_auto, row_up_ts = cmemo[1]
596
+ parsed_title, parsed_first, row_auto_by, row_up_ts = cmemo[1]
581
597
  else:
582
598
  parsed_title = snippet(raw_title, 200)
583
599
  parsed_first = snippet(clean_prompt(raw_first, "codex"))
584
- row_is_auto = bool(is_codex_automated(csrc, raw_title, raw_first) or row.get("agent_role"))
600
+ row_auto_by = "source" if row.get("agent_role") else codex_automation_reason(csrc, raw_title, raw_first, row.get("originator"))
585
601
  row_up_ts = stamp(row.get("updated_at"))
586
- self._codex_row_memo[rid] = (codex_sig, (parsed_title, parsed_first, row_is_auto, row_up_ts))
602
+ self._codex_row_memo[rid] = (codex_sig, (parsed_title, parsed_first, row_auto_by, row_up_ts))
603
+ row_is_auto = bool(row_auto_by)
587
604
  path = row.get("rollout_path")
588
605
  resolved_path = None
589
606
  if path:
@@ -604,6 +621,8 @@ class DiscoveryMixin:
604
621
  "firstPrompt": parsed_first,
605
622
  "model": row.get("model"), "reasoningEffort": row.get("reasoning_effort"),
606
623
  "automated": is_auto})
624
+ if row_auto_by:
625
+ s["automatedBy"] = row_auto_by
607
626
  if resolved_path:
608
627
  s["path"] = resolved_path
609
628
  known_codex_paths.add(resolved_path)
@@ -853,9 +872,13 @@ class DiscoveryMixin:
853
872
  s.update(isLive=True, synthetic=synthetic)
854
873
  s["cwd"] = info.get("cwd") or opened.get("cwd") or s["cwd"]
855
874
  s["status"] = info.get("status") or s.get("status", "unknown")
875
+ if info.get("_parkedDone"):
876
+ s["parkedDone"] = True
856
877
  if tool == "claude" and info:
857
878
  lock_ts = max(float(info.get("_mtime") or 0), stamp(info.get("updatedAt")), stamp(info.get("startedAt")))
858
879
  s["lockMtime"] = lock_ts
880
+ if info.get("version"):
881
+ s["cliVersion"] = str(info["version"])
859
882
  if lock_ts > 0 and not s.get("updatedAt"):
860
883
  s["updatedAt"] = lock_ts
861
884
  elif tool in active_locks and sid in active_locks[tool]:
@@ -952,6 +975,8 @@ class DiscoveryMixin:
952
975
  s["isLive"] = True
953
976
  lock_ts = max(float(info.get("_mtime") or 0), stamp(info.get("updatedAt")), stamp(info.get("startedAt")))
954
977
  s["lockMtime"] = lock_ts
978
+ if info.get("version"):
979
+ s["cliVersion"] = str(info["version"])
955
980
  if lock_ts > 0 and not s.get("updatedAt"):
956
981
  s["updatedAt"] = lock_ts
957
982
  if not any(pr.get("pid") == pid for pr in s["processes"]):
@@ -1060,6 +1085,8 @@ class DiscoveryMixin:
1060
1085
  not_idle = bool(s.get("notFullyIdle") and not s.get("killed") and age < 1800)
1061
1086
  if raw_status in ("working", "running", "busy") or ("running" in raw_status and age < 1800) or not_idle:
1062
1087
  s["agentState"] = "working"
1088
+ elif s.get("parkedDone"):
1089
+ s["agentState"] = "done_idle"
1063
1090
  elif not has_procs:
1064
1091
  s["agentState"] = "done_idle"
1065
1092
  if tstat in ("ball_in_model", "unclosed_tool"):
@@ -1174,10 +1201,9 @@ class DiscoveryMixin:
1174
1201
  exit_rc = int(exit_info.get("rc", 0))
1175
1202
  except (TypeError, ValueError, AttributeError):
1176
1203
  exit_rc = 0
1177
- signal_exit = exit_rc < 0 or exit_rc >= 128 or bool(
1178
- isinstance(exit_info, dict) and exit_info.get("signal_exit")
1179
- )
1180
- if not signal_exit and exit_ts > 0.0 and exit_ts >= sess_ts - 5.0:
1204
+ # Only rc 0 is the user leaving (`/exit`); a signal or a CLI that failed to start is not
1205
+ user_exit = exit_rc == 0 and not (isinstance(exit_info, dict) and exit_info.get("signal_exit"))
1206
+ if user_exit and exit_ts > 0.0 and exit_ts >= sess_ts - 5.0:
1181
1207
  s["isLive"] = False
1182
1208
  s["inWorkingSet"] = False
1183
1209
  s["agentState"] = "dormant"
@@ -1187,7 +1213,12 @@ class DiscoveryMixin:
1187
1213
  wset_dirty = True
1188
1214
  wset_removed.append((key, f"exited rc={exit_rc}"))
1189
1215
  continue
1190
- if s.get("isLive"):
1216
+ if s.get("isLive") and not s.get("processes") and not s.get("resident") \
1217
+ and not int(s.get("activeSubagents") or 0) and key not in wset and s["id"] not in wset:
1218
+ # Live by a Codex / AGY lock alone, with no interactive process: a one-shot run another agent
1219
+ # started (`agy -p`, a Codex task). It shows in `Recent` and never enters `Active`.
1220
+ s["inWorkingSet"] = False
1221
+ elif s.get("isLive"):
1191
1222
  s["inWorkingSet"] = True
1192
1223
  if key not in wset:
1193
1224
  wset.add(key)
@@ -7,6 +7,8 @@ from __future__ import annotations
7
7
 
8
8
  import subprocess
9
9
 
10
+ from mixhub.hub.shell import resume_cwd
11
+
10
12
 
11
13
  def find_controlled_tmux_pane(session: dict, tmux_lister=None) -> str:
12
14
  """
@@ -73,7 +75,7 @@ def send_to_session(
73
75
  resolved_tool = session["tool"]
74
76
  resolved_id = session["id"]
75
77
  skey = f"{resolved_tool}:{resolved_id}"
76
- cwd = session.get("cwd") or home
78
+ cwd = resume_cwd(session) if session.get("path") else (session.get("cwd") or home)
77
79
 
78
80
  clean_files = [str(p).strip() for p in (attachments or []) if str(p).strip()]
79
81
  prompt_parts = [str(text or "").strip()] if str(text or "").strip() else []
@@ -13,31 +13,36 @@ from mixhub.hub.titles import FALLBACK_TITLE_RE
13
13
  from mixhub.hub.util import read_json
14
14
 
15
15
 
16
- CODEX_AUTO_PREFIXES = (
17
- "Read-only ", "READ FIRST", "Read the following key files", "Read " + os.path.expanduser("~") + "/",
18
- "Repository: ", "Adversarial second opinion", "Second opinion on what a trading bot",
19
- "Use image_gen to generate", "请先用 view_image 读取", "请用 image_gen 生成",
20
- "Create a self-contained HTML", "读取以下文件并完成中文文案重写任务",
21
- "# Files mentioned by the user:\n\n## Photo 1.jpg: /tmp/codex-remote-attachments/",
22
- )
23
-
24
-
25
- def is_codex_automated(source=None, title="", first_msg=""):
16
+ def codex_automation_reason(source=None, title="", first_msg="", originator=None) -> str:
17
+ """
18
+ Why a Codex thread counts as automated, or "" when it doesn't. What Codex recorded about who started the thread
19
+ decides first (`originator`, `source`); text rules only cover threads older than those fields.
20
+ """
21
+ # A task Claude Code dispatched to Codex (codex-companion); its result goes back to that Claude session
22
+ if originator == "Claude Code":
23
+ return "originator"
26
24
  if isinstance(source, dict) or (isinstance(source, str) and (source == "exec" or source.startswith("{"))):
27
- return True
25
+ return "source"
28
26
  t = (title or "").strip()
29
27
  m = (first_msg or "").strip()
30
- if "<task>" in m or "<task>" in t or t.startswith("Codex Companion Task:"):
31
- return True
32
- if m.startswith("## Referenced ChatGPT conversation:") or t.startswith("## Referenced ChatGPT conversation:"):
33
- return True
34
- if m in ("--help", "exit", "usage") or t in ("--help", "exit", "usage"):
35
- return True
36
- if m.startswith(CODEX_AUTO_PREFIXES):
37
- return True
38
- if is_automated_or_subagent_text(t) or is_automated_or_subagent_text(m):
39
- return True
40
- return False
28
+ if "<task>" in m or "<task>" in t:
29
+ return "text:<task>"
30
+ for prefix in ("Codex Companion Task:", "## Referenced ChatGPT conversation:"):
31
+ if m.startswith(prefix) or t.startswith(prefix):
32
+ return "text:" + prefix
33
+ for word in ("--help", "exit", "usage"):
34
+ if word in (m, t):
35
+ return "text:" + word
36
+ low = m.casefold()
37
+ for prefix in _load_local_rules().get("codex_automated_prefixes") or ():
38
+ if low.startswith(prefix.casefold()):
39
+ return "text:" + prefix
40
+ rule = automated_text_rule(t) or automated_text_rule(m)
41
+ return "text:" + rule if rule else ""
42
+
43
+
44
+ def is_codex_automated(source=None, title="", first_msg="", originator=None):
45
+ return bool(codex_automation_reason(source, title, first_msg, originator))
41
46
 
42
47
 
43
48
  _DISCOVER_GEN: int = 0
@@ -113,7 +118,7 @@ def _load_local_rules() -> dict:
113
118
  if not isinstance(raw, dict):
114
119
  raw = {}
115
120
  data = {}
116
- for key in ("automated_prefixes", "automated_substrings", "relayed_prefixes"):
121
+ for key in ("automated_prefixes", "automated_substrings", "relayed_prefixes", "codex_automated_prefixes"):
117
122
  vals = raw.get(key)
118
123
  data[key] = [str(v) for v in vals if isinstance(v, str) and v.strip()] if isinstance(vals, list) else []
119
124
  patterns = prefs.get("ignore_prompt_patterns")
@@ -135,24 +140,35 @@ def _load_local_rules() -> dict:
135
140
  return data
136
141
 
137
142
 
138
- def is_automated_or_subagent_text(text: str) -> bool:
143
+ def automated_text_rule(text: str) -> str:
144
+ """The built-in or local rule that marks `text` as automation, or "" when none does."""
139
145
  if not text:
140
- return False
146
+ return ""
141
147
  s = str(text).strip()
142
148
  if not s:
143
- return False
144
- if s.startswith(_BUILTIN_AUTOMATED_PREFIXES) or any(m in s for m in _BUILTIN_AUTOMATED_SUBSTRINGS):
145
- return True
149
+ return ""
150
+ for p in _BUILTIN_AUTOMATED_PREFIXES:
151
+ if s.startswith(p):
152
+ return p
153
+ for m in _BUILTIN_AUTOMATED_SUBSTRINGS:
154
+ if m in s:
155
+ return m
146
156
  low = s.casefold()
147
157
  rules = _load_local_rules()
148
158
  # Local rules match case-insensitively (casefold) so user-supplied patterns need not mirror exact casing.
149
159
  prefixes = _LOCAL_RULES_CACHE.get("folded_prefixes") or tuple(p.casefold() for p in (rules.get("automated_prefixes") or []))
150
- if prefixes and low.startswith(prefixes):
151
- return True
160
+ for p in prefixes:
161
+ if low.startswith(p):
162
+ return p
152
163
  substrings = _LOCAL_RULES_CACHE.get("folded_substrings") or tuple(m.casefold() for m in (rules.get("automated_substrings") or []))
153
- if substrings and any(m in low for m in substrings):
154
- return True
155
- return False
164
+ for m in substrings:
165
+ if m in low:
166
+ return "has:" + m
167
+ return ""
168
+
169
+
170
+ def is_automated_or_subagent_text(text: str) -> bool:
171
+ return bool(automated_text_rule(text))
156
172
 
157
173
 
158
174
  def is_users_own_prompt(text: str) -> bool:
@@ -0,0 +1,85 @@
1
+ """Subprocess runner and Terminal helpers: `run`, AppleScript quoting, the Terminal focus script, resume commands."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import re
7
+ import shlex
8
+ import subprocess
9
+
10
+ from mixhub.proc.identity import PS_ENV
11
+
12
+
13
+ def run(args, timeout=5):
14
+ env = {**os.environ, **PS_ENV} if (args and os.path.basename(str(args[0])) == "ps") else None
15
+ result = subprocess.run(args, capture_output=True, text=True, timeout=timeout, env=env)
16
+ if result.returncode:
17
+ raise RuntimeError(result.stderr.strip()[:400] or "Command failed: " + args[0])
18
+ return result.stdout
19
+
20
+
21
+ _DEFAULT_RUN = run
22
+
23
+
24
+ def apple_string(text):
25
+ return '"' + text.replace("\\", "\\\\").replace('"', '\\"').replace("\r", "\\r").replace("\n", "\\n") + '"'
26
+
27
+
28
+ def focus_script(tty):
29
+ return '''tell application "Terminal"
30
+ repeat with w in windows
31
+ repeat with t in tabs of w
32
+ if tty of t is %s then
33
+ set selected tab of w to t
34
+ set index of w to 1
35
+ activate
36
+ return "focused"
37
+ end if
38
+ end repeat
39
+ end repeat
40
+ error "No Terminal tab matches this tty"
41
+ end tell''' % apple_string("/dev/" + tty.removeprefix("/dev/"))
42
+
43
+
44
+ def _claude_project_dir_name(cwd: str) -> str:
45
+ return re.sub(r"[^A-Za-z0-9]", "-", cwd)
46
+
47
+
48
+ def resume_cwd(session: dict) -> str:
49
+ """
50
+ Folder to resume `session` from. Claude Code looks a transcript up by the folder it runs in, so a session whose
51
+ cwd moved after it started (`cd` inside it) resumes from a folder its transcript belongs to: the session's own
52
+ cwd when that matches, else the first cwd its transcript records that does. Other tools use the session's cwd.
53
+ """
54
+ home = os.path.expanduser("~")
55
+ cwd = session.get("cwd") or home
56
+ path = str(session.get("path") or "")
57
+ if session.get("tool") != "claude" or not path:
58
+ return cwd
59
+ want = os.path.basename(os.path.dirname(path))
60
+ if _claude_project_dir_name(cwd) == want:
61
+ return cwd
62
+ try:
63
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
64
+ for _, line in zip(range(2000), f):
65
+ if '"cwd"' not in line:
66
+ continue
67
+ try:
68
+ cand = json.loads(line).get("cwd")
69
+ except ValueError:
70
+ continue
71
+ if isinstance(cand, str) and _claude_project_dir_name(cand) == want and os.path.isdir(cand):
72
+ return cand
73
+ except OSError:
74
+ pass
75
+ return home if _claude_project_dir_name(home) == want else cwd
76
+
77
+
78
+ def resume_command(session):
79
+ tool, sid = session["tool"], session["id"]
80
+ if session.get("synthetic"):
81
+ suffix = tool
82
+ else:
83
+ suffix = {"claude": "claude --resume ", "agy": "agy --conversation ", "codex": "codex resume ",
84
+ "opencode": "opencode -s "}[tool] + shlex.quote(sid)
85
+ return "cd " + shlex.quote(resume_cwd(session)) + " && " + suffix
@@ -17,7 +17,7 @@ from mixhub.hub.constants import (
17
17
  )
18
18
  from mixhub.hub.opencode import opencode_entries
19
19
  from mixhub.hub.schema import claude_is_headless_marker, claude_is_synthetic_user, PROVIDER_SCHEMA_ADAPTERS
20
- from mixhub.hub.session_filters import is_automated_or_subagent_text, is_codex_automated
20
+ from mixhub.hub.session_filters import codex_automation_reason, is_automated_or_subagent_text
21
21
  from mixhub.hub.text import clean_prompt, snippet
22
22
  from mixhub.hub.titles import (
23
23
  _is_generic_placeholder_title,
@@ -266,8 +266,10 @@ class TranscriptMixin:
266
266
  if row.get("type") == "session_meta":
267
267
  meta = row.get("payload") or {}
268
268
  result.update({k: meta[k] for k in ("id", "cwd") if meta.get(k)})
269
- if is_codex_automated(meta.get("source")):
269
+ auto_by = codex_automation_reason(meta.get("source"), originator=meta.get("originator"))
270
+ if auto_by:
270
271
  result["automated"] = True
272
+ result["automatedBy"] = auto_by
271
273
  if row.get("lastPrompt"):
272
274
  result["lastPrompt"] = snippet(clean_prompt(row["lastPrompt"], tool), 220)
273
275
  if has_sdk_cli and not has_human_turn:
@@ -302,8 +304,11 @@ class TranscriptMixin:
302
304
  result["messageCount"] += 1
303
305
  if entry["role"] == "user" and not entry.get("synthetic"):
304
306
  prompt = snippet(entry["text"])
305
- if tool == "codex" and not result["firstPrompt"] and is_codex_automated(None, "", entry["text"]):
306
- result["automated"] = True
307
+ if tool == "codex" and not result["firstPrompt"]:
308
+ auto_by = codex_automation_reason(None, "", entry["text"])
309
+ if auto_by:
310
+ result["automated"] = True
311
+ result.setdefault("automatedBy", auto_by)
307
312
  result["firstPrompt"] = result["firstPrompt"] or prompt
308
313
  if not substantive_prompt and not _is_generic_placeholder_title(prompt) and len(prompt.strip()) >= 4:
309
314
  substantive_prompt = prompt