oh-my-codex 0.18.7 → 0.18.8

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 (259) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +5 -5
  4. package/crates/omx-sparkshell/tests/execution.rs +1 -1
  5. package/dist/agents/__tests__/native-config.test.js +42 -1
  6. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  7. package/dist/agents/definitions.d.ts +8 -0
  8. package/dist/agents/definitions.d.ts.map +1 -1
  9. package/dist/agents/definitions.js +1 -0
  10. package/dist/agents/definitions.js.map +1 -1
  11. package/dist/agents/native-config.d.ts +5 -1
  12. package/dist/agents/native-config.d.ts.map +1 -1
  13. package/dist/agents/native-config.js +17 -2
  14. package/dist/agents/native-config.js.map +1 -1
  15. package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
  16. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  17. package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
  18. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  19. package/dist/cli/__tests__/index.test.js +61 -5
  20. package/dist/cli/__tests__/index.test.js.map +1 -1
  21. package/dist/cli/__tests__/package-bin-contract.test.js +8 -4
  22. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  23. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
  24. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  25. package/dist/cli/__tests__/ralph.test.js +14 -0
  26. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  27. package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
  28. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  29. package/dist/cli/__tests__/setup-refresh.test.js +65 -0
  30. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  31. package/dist/cli/__tests__/state.test.js +21 -0
  32. package/dist/cli/__tests__/state.test.js.map +1 -1
  33. package/dist/cli/__tests__/team.test.js +2 -2
  34. package/dist/cli/__tests__/update.test.js +110 -2
  35. package/dist/cli/__tests__/update.test.js.map +1 -1
  36. package/dist/cli/doctor.d.ts.map +1 -1
  37. package/dist/cli/doctor.js +8 -1
  38. package/dist/cli/doctor.js.map +1 -1
  39. package/dist/cli/index.d.ts +11 -2
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +108 -15
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/cli/plugin-marketplace.d.ts +14 -2
  44. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  45. package/dist/cli/plugin-marketplace.js +62 -15
  46. package/dist/cli/plugin-marketplace.js.map +1 -1
  47. package/dist/cli/ralph.d.ts.map +1 -1
  48. package/dist/cli/ralph.js +3 -1
  49. package/dist/cli/ralph.js.map +1 -1
  50. package/dist/cli/setup-preferences.d.ts +2 -0
  51. package/dist/cli/setup-preferences.d.ts.map +1 -1
  52. package/dist/cli/setup-preferences.js +4 -0
  53. package/dist/cli/setup-preferences.js.map +1 -1
  54. package/dist/cli/setup.d.ts +3 -0
  55. package/dist/cli/setup.d.ts.map +1 -1
  56. package/dist/cli/setup.js +166 -27
  57. package/dist/cli/setup.js.map +1 -1
  58. package/dist/cli/state.d.ts.map +1 -1
  59. package/dist/cli/state.js +8 -1
  60. package/dist/cli/state.js.map +1 -1
  61. package/dist/cli/tmux-hook.d.ts.map +1 -1
  62. package/dist/cli/tmux-hook.js +16 -0
  63. package/dist/cli/tmux-hook.js.map +1 -1
  64. package/dist/cli/update.d.ts +2 -0
  65. package/dist/cli/update.d.ts.map +1 -1
  66. package/dist/cli/update.js +47 -3
  67. package/dist/cli/update.js.map +1 -1
  68. package/dist/config/__tests__/generator-notify.test.js +1 -0
  69. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  70. package/dist/config/generator.d.ts +2 -2
  71. package/dist/config/generator.d.ts.map +1 -1
  72. package/dist/config/generator.js +2 -2
  73. package/dist/config/generator.js.map +1 -1
  74. package/dist/config/team-mode.d.ts +12 -0
  75. package/dist/config/team-mode.d.ts.map +1 -0
  76. package/dist/config/team-mode.js +91 -0
  77. package/dist/config/team-mode.js.map +1 -0
  78. package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
  79. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  80. package/dist/hooks/__tests__/code-review-skill-contract.test.js +8 -0
  81. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  82. package/dist/hooks/__tests__/keyword-detector.test.js +423 -3
  83. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  84. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +1 -1
  85. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  86. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
  87. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  88. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
  89. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  90. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
  91. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  92. package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
  93. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  94. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  95. package/dist/hooks/agents-overlay.js +36 -50
  96. package/dist/hooks/agents-overlay.js.map +1 -1
  97. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
  98. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  99. package/dist/hooks/extensibility/plugin-runner.js +17 -21
  100. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  101. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  102. package/dist/hooks/keyword-detector.js +258 -12
  103. package/dist/hooks/keyword-detector.js.map +1 -1
  104. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  105. package/dist/hooks/prompt-guidance-contract.js +6 -0
  106. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  107. package/dist/hooks/session.d.ts +1 -0
  108. package/dist/hooks/session.d.ts.map +1 -1
  109. package/dist/hooks/session.js.map +1 -1
  110. package/dist/hud/__tests__/authority.test.js +435 -32
  111. package/dist/hud/__tests__/authority.test.js.map +1 -1
  112. package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
  113. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  114. package/dist/hud/__tests__/index.test.js +42 -0
  115. package/dist/hud/__tests__/index.test.js.map +1 -1
  116. package/dist/hud/__tests__/reconcile.test.js +521 -15
  117. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  118. package/dist/hud/__tests__/render.test.js +61 -0
  119. package/dist/hud/__tests__/render.test.js.map +1 -1
  120. package/dist/hud/__tests__/state.test.js +132 -4
  121. package/dist/hud/__tests__/state.test.js.map +1 -1
  122. package/dist/hud/__tests__/tmux.test.js +180 -21
  123. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  124. package/dist/hud/authority.d.ts +5 -0
  125. package/dist/hud/authority.d.ts.map +1 -1
  126. package/dist/hud/authority.js +324 -28
  127. package/dist/hud/authority.js.map +1 -1
  128. package/dist/hud/index.d.ts +3 -2
  129. package/dist/hud/index.d.ts.map +1 -1
  130. package/dist/hud/index.js +42 -19
  131. package/dist/hud/index.js.map +1 -1
  132. package/dist/hud/reconcile.d.ts +3 -3
  133. package/dist/hud/reconcile.d.ts.map +1 -1
  134. package/dist/hud/reconcile.js +128 -19
  135. package/dist/hud/reconcile.js.map +1 -1
  136. package/dist/hud/render.d.ts.map +1 -1
  137. package/dist/hud/render.js +35 -0
  138. package/dist/hud/render.js.map +1 -1
  139. package/dist/hud/state.d.ts.map +1 -1
  140. package/dist/hud/state.js +61 -62
  141. package/dist/hud/state.js.map +1 -1
  142. package/dist/hud/tmux.d.ts +24 -6
  143. package/dist/hud/tmux.d.ts.map +1 -1
  144. package/dist/hud/tmux.js +136 -38
  145. package/dist/hud/tmux.js.map +1 -1
  146. package/dist/hud/types.d.ts +11 -0
  147. package/dist/hud/types.d.ts.map +1 -1
  148. package/dist/hud/types.js.map +1 -1
  149. package/dist/mcp/__tests__/state-paths.test.js +71 -1
  150. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  151. package/dist/mcp/state-paths.d.ts +32 -0
  152. package/dist/mcp/state-paths.d.ts.map +1 -1
  153. package/dist/mcp/state-paths.js +113 -17
  154. package/dist/mcp/state-paths.js.map +1 -1
  155. package/dist/mcp/state-server.d.ts +4 -4
  156. package/dist/scripts/__tests__/codex-native-hook.test.js +593 -11
  157. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  158. package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
  159. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  160. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
  161. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
  162. package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
  163. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
  164. package/dist/scripts/__tests__/run-test-files.test.js +74 -0
  165. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  166. package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
  167. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  168. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  169. package/dist/scripts/codex-native-hook.js +88 -31
  170. package/dist/scripts/codex-native-hook.js.map +1 -1
  171. package/dist/scripts/eval/eval-parity-smoke.js +1 -1
  172. package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
  173. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  174. package/dist/scripts/notify-hook/auto-nudge.js +3 -1
  175. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  176. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  177. package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
  178. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  179. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  180. package/dist/scripts/notify-hook/state-io.js +62 -38
  181. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  182. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  183. package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
  184. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  185. package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
  186. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  187. package/dist/scripts/notify-hook/tmux-injection.js +24 -18
  188. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  189. package/dist/scripts/notify-hook.js +75 -11
  190. package/dist/scripts/notify-hook.js.map +1 -1
  191. package/dist/scripts/run-test-files.js +193 -22
  192. package/dist/scripts/run-test-files.js.map +1 -1
  193. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  194. package/dist/scripts/sync-plugin-mirror.js +61 -3
  195. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  196. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  197. package/dist/scripts/verify-native-agents.js +58 -1
  198. package/dist/scripts/verify-native-agents.js.map +1 -1
  199. package/dist/state/__tests__/operations.test.js +113 -0
  200. package/dist/state/__tests__/operations.test.js.map +1 -1
  201. package/dist/state/__tests__/skill-active.test.js +3 -16
  202. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  203. package/dist/state/__tests__/workflow-transition.test.js +25 -0
  204. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  205. package/dist/state/operations.d.ts.map +1 -1
  206. package/dist/state/operations.js +57 -2
  207. package/dist/state/operations.js.map +1 -1
  208. package/dist/state/skill-active.d.ts.map +1 -1
  209. package/dist/state/skill-active.js +7 -39
  210. package/dist/state/skill-active.js.map +1 -1
  211. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  212. package/dist/state/workflow-transition-reconcile.js +10 -14
  213. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  214. package/dist/team/__tests__/runtime.test.js +1 -1
  215. package/dist/team/__tests__/runtime.test.js.map +1 -1
  216. package/dist/team/__tests__/scaling.test.js +9 -4
  217. package/dist/team/__tests__/scaling.test.js.map +1 -1
  218. package/dist/team/__tests__/tmux-session.test.js +195 -2
  219. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  220. package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
  221. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  222. package/dist/team/scaling.d.ts.map +1 -1
  223. package/dist/team/scaling.js +3 -2
  224. package/dist/team/scaling.js.map +1 -1
  225. package/dist/team/tmux-session.d.ts +2 -0
  226. package/dist/team/tmux-session.d.ts.map +1 -1
  227. package/dist/team/tmux-session.js +142 -12
  228. package/dist/team/tmux-session.js.map +1 -1
  229. package/dist/verification/__tests__/ci-rust-gates.test.js +81 -1
  230. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  231. package/package.json +8 -8
  232. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  233. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
  234. package/plugins/oh-my-codex/hooks/hooks.json +1 -2
  235. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -1
  236. package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
  237. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
  238. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
  239. package/skills/autopilot/SKILL.md +3 -1
  240. package/skills/code-review/SKILL.md +7 -7
  241. package/skills/ralph/SKILL.md +22 -22
  242. package/skills/ultraqa/SKILL.md +9 -0
  243. package/src/scripts/__tests__/codex-native-hook.test.ts +686 -13
  244. package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
  245. package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
  246. package/src/scripts/__tests__/run-test-files.test.ts +102 -0
  247. package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
  248. package/src/scripts/codex-native-hook.ts +105 -28
  249. package/src/scripts/demo-team-e2e.sh +10 -7
  250. package/src/scripts/eval/eval-parity-smoke.ts +1 -1
  251. package/src/scripts/notify-hook/auto-nudge.ts +3 -1
  252. package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
  253. package/src/scripts/notify-hook/state-io.ts +75 -37
  254. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
  255. package/src/scripts/notify-hook/tmux-injection.ts +35 -19
  256. package/src/scripts/notify-hook.ts +91 -4
  257. package/src/scripts/run-test-files.ts +192 -22
  258. package/src/scripts/sync-plugin-mirror.ts +98 -9
  259. package/src/scripts/verify-native-agents.ts +65 -1
@@ -84,6 +84,139 @@ describe('reconcileHudForPromptSubmit', () => {
84
84
  assert.equal(resized.length, 1);
85
85
  assert.equal(resized[0]?.heightLines, HUD_TMUX_HEIGHT_LINES);
86
86
  });
87
+ it('reaps orphaned same-session HUD panes whose leader pane was destroyed, then recreates a single HUD', async () => {
88
+ // Regression for the "team mode leaves only stacked HUD strips" bug: the leader
89
+ // pane (%21) was destroyed but its owner-tagged HUD panes remained, all pointing
90
+ // at the dead leader id. They match neither findHudWatchPaneIds (leader mismatch)
91
+ // nor findLegacyFocusedHudWatchPaneIds (they carry owner metadata), so each prompt
92
+ // submit previously appended a fresh HUD instead of reclaiming the orphans.
93
+ const killed = [];
94
+ const created = [];
95
+ const orphan = (paneId) => ({
96
+ paneId,
97
+ currentCommand: 'node',
98
+ startCommand: `exec env OMX_SESSION_ID='sess-a' OMX_TMUX_HUD_OWNER='1' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%21' node omx hud --watch --preset=focused`,
99
+ });
100
+ const result = await reconcileHudForPromptSubmit('/repo', {
101
+ env: { TMUX: '1', TMUX_PANE: '%33', OMX_SESSION_ID: 'sess-a', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
102
+ listCurrentWindowPanes: () => [
103
+ // %33 is the current (live) leader pane; %21 is gone from the window.
104
+ { paneId: '%33', currentCommand: 'codex', startCommand: 'codex' },
105
+ orphan('%34'),
106
+ orphan('%42'),
107
+ orphan('%47'),
108
+ ],
109
+ killTmuxPane: (paneId) => {
110
+ killed.push(paneId);
111
+ return true;
112
+ },
113
+ resizeTmuxPane: () => true,
114
+ createHudWatchPane: (_cwd, cmd, options) => {
115
+ created.push({ cmd, options });
116
+ return '%50';
117
+ },
118
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
119
+ });
120
+ // All three dead-leader orphans are reaped, then exactly one fresh HUD is created.
121
+ assert.deepEqual(killed.sort(), ['%34', '%42', '%47']);
122
+ assert.equal(result.status, 'recreated');
123
+ assert.equal(result.paneId, '%50');
124
+ assert.equal(created.length, 1);
125
+ assert.equal(created[0]?.options?.targetPaneId, '%33');
126
+ assert.match(created[0]?.cmd || '', new RegExp(`${OMX_TMUX_HUD_LEADER_PANE_ENV}='%33'`));
127
+ });
128
+ it('reaps orphaned HUD panes tagged with an equivalent native session id', async () => {
129
+ // #2684 lets HUD dedupe treat the OMX owner id and Codex native session id as
130
+ // equivalent. Orphan reaping must use the same identity set so a canonical
131
+ // owner reconcile still reclaims dead-leader HUDs tagged with the native id.
132
+ const killed = [];
133
+ const result = await reconcileHudForPromptSubmit('/repo', {
134
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'codex-native-uuid', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
135
+ sessionId: 'omx-owner-abc',
136
+ sessionIds: ['omx-owner-abc', 'codex-native-uuid'],
137
+ listCurrentWindowPanes: () => [
138
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
139
+ {
140
+ paneId: '%2',
141
+ currentCommand: 'node',
142
+ startCommand: `env OMX_SESSION_ID='codex-native-uuid' OMX_TMUX_HUD_OWNER='1' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%21' node omx hud --watch`,
143
+ },
144
+ ],
145
+ killTmuxPane: (paneId) => {
146
+ killed.push(paneId);
147
+ return true;
148
+ },
149
+ resizeTmuxPane: () => true,
150
+ createHudWatchPane: () => '%9',
151
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
152
+ });
153
+ assert.deepEqual(killed, ['%2']);
154
+ assert.equal(result.status, 'recreated');
155
+ assert.equal(result.paneId, '%9');
156
+ });
157
+ it('does not reap an orphaned HUD pane that belongs to a different session', async () => {
158
+ // A HUD owned by another session's leader (which may live in a different tmux
159
+ // window we cannot see here) must survive even when that leader is absent.
160
+ const killed = [];
161
+ const result = await reconcileHudForPromptSubmit('/repo', {
162
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-a', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
163
+ listCurrentWindowPanes: () => [
164
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
165
+ {
166
+ paneId: '%4',
167
+ currentCommand: 'node',
168
+ startCommand: `env OMX_SESSION_ID='sess-b' OMX_TMUX_HUD_OWNER='1' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%5' node omx hud --watch`,
169
+ },
170
+ ],
171
+ killTmuxPane: (paneId) => {
172
+ killed.push(paneId);
173
+ return true;
174
+ },
175
+ resizeTmuxPane: () => true,
176
+ createHudWatchPane: () => '%9',
177
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
178
+ });
179
+ // sess-b orphan is left untouched; this session simply creates its own HUD.
180
+ assert.deepEqual(killed, []);
181
+ assert.equal(result.status, 'recreated');
182
+ assert.equal(result.paneId, '%9');
183
+ });
184
+ it('reaps a same-session orphan whose recorded leader is itself another HUD pane', async () => {
185
+ // Review follow-up (#2682): when a HUD pane was mistakenly used as a leader, an
186
+ // orphan can name another HUD pane as its leader. That referenced HUD must not
187
+ // count as a live leader, or the orphan survives while the referenced HUD is
188
+ // reaped — leaving a dangling strip that still never matches the real pane.
189
+ const killed = [];
190
+ const result = await reconcileHudForPromptSubmit('/repo', {
191
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-a', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
192
+ listCurrentWindowPanes: () => [
193
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
194
+ {
195
+ // orphan whose recorded leader (%3) is itself another HUD pane
196
+ paneId: '%2',
197
+ currentCommand: 'node',
198
+ startCommand: `exec env OMX_SESSION_ID='sess-a' OMX_TMUX_HUD_OWNER='1' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%3' node omx hud --watch --preset=focused`,
199
+ },
200
+ {
201
+ // the referenced HUD %3, itself orphaned (its leader %21 is gone)
202
+ paneId: '%3',
203
+ currentCommand: 'node',
204
+ startCommand: `exec env OMX_SESSION_ID='sess-a' OMX_TMUX_HUD_OWNER='1' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%21' node omx hud --watch --preset=focused`,
205
+ },
206
+ ],
207
+ killTmuxPane: (paneId) => {
208
+ killed.push(paneId);
209
+ return true;
210
+ },
211
+ resizeTmuxPane: () => true,
212
+ createHudWatchPane: () => '%9',
213
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
214
+ });
215
+ // Both HUD-led and dead-leader orphans are reaped; a single fresh HUD is created.
216
+ assert.deepEqual(killed.sort(), ['%2', '%3']);
217
+ assert.equal(result.status, 'recreated');
218
+ assert.equal(result.paneId, '%9');
219
+ });
87
220
  it('prefers an explicit session override when recreating HUD', async () => {
88
221
  const created = [];
89
222
  const result = await reconcileHudForPromptSubmit('/repo', {
@@ -128,6 +261,87 @@ describe('reconcileHudForPromptSubmit', () => {
128
261
  assert.equal(created.length, 1);
129
262
  assert.match(created[0]?.cmd || '', /^exec env OMX_SESSION_ID='sess boxed' OMX_TMUX_HUD_OWNER='1' OMX_TMUX_HUD_LEADER_PANE='%1' OMX_ROOT='\/tmp\/boxed root\/it'\\''s\/\$\(literal\)' '.*' '.*omx\.js' hud --watch/);
130
263
  });
264
+ it('forwards OMX_STATE_ROOT when recreating HUD with shell-safe quoting', async () => {
265
+ const created = [];
266
+ const result = await reconcileHudForPromptSubmit('/repo', {
267
+ env: {
268
+ TMUX: '1',
269
+ TMUX_PANE: '%1',
270
+ OMX_SESSION_ID: 'sess-a',
271
+ OMX_STATE_ROOT: '/boxed state/root',
272
+ [OMX_TMUX_HUD_OWNER_ENV]: '1',
273
+ },
274
+ listCurrentWindowPanes: () => [{ paneId: '%1', currentCommand: 'codex', startCommand: 'codex' }],
275
+ createHudWatchPane: (_cwd, hudCmd) => {
276
+ created.push(hudCmd);
277
+ return '%9';
278
+ },
279
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
280
+ resizeTmuxPane: () => true,
281
+ readHudConfig: async () => ({ preset: 'focused', git: { display: 'branch' }, statusLine: { preset: 'focused' } }),
282
+ readAllState: async () => ({
283
+ version: null,
284
+ gitBranch: null,
285
+ ralph: null,
286
+ ultragoal: null,
287
+ ultrawork: null,
288
+ autopilot: null,
289
+ ralplan: null,
290
+ deepInterview: null,
291
+ autoresearch: null,
292
+ ultraqa: null,
293
+ team: null,
294
+ metrics: null,
295
+ hudNotify: null,
296
+ session: null,
297
+ }),
298
+ });
299
+ assert.equal(result.status, 'recreated');
300
+ assert.match(created[0] ?? '', /OMX_STATE_ROOT='\/boxed state\/root'/);
301
+ assert.doesNotMatch(created[0] ?? '', /OMX_ROOT=/);
302
+ });
303
+ it('forwards OMX_TEAM_STATE_ROOT before boxed roots when recreating HUD', async () => {
304
+ const created = [];
305
+ const result = await reconcileHudForPromptSubmit('/repo', {
306
+ env: {
307
+ TMUX: '1',
308
+ TMUX_PANE: '%1',
309
+ OMX_SESSION_ID: 'sess-a',
310
+ OMX_ROOT: '/boxed-root',
311
+ OMX_STATE_ROOT: '/boxed-state-root',
312
+ OMX_TEAM_STATE_ROOT: '/team-state-root',
313
+ [OMX_TMUX_HUD_OWNER_ENV]: '1',
314
+ },
315
+ listCurrentWindowPanes: () => [{ paneId: '%1', currentCommand: 'codex', startCommand: 'codex' }],
316
+ createHudWatchPane: (_cwd, hudCmd) => {
317
+ created.push(hudCmd);
318
+ return '%9';
319
+ },
320
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
321
+ resizeTmuxPane: () => true,
322
+ readHudConfig: async () => ({ preset: 'focused', git: { display: 'branch' }, statusLine: { preset: 'focused' } }),
323
+ readAllState: async () => ({
324
+ version: null,
325
+ gitBranch: null,
326
+ ralph: null,
327
+ ultragoal: null,
328
+ ultrawork: null,
329
+ autopilot: null,
330
+ ralplan: null,
331
+ deepInterview: null,
332
+ autoresearch: null,
333
+ ultraqa: null,
334
+ team: null,
335
+ metrics: null,
336
+ hudNotify: null,
337
+ session: null,
338
+ }),
339
+ });
340
+ assert.equal(result.status, 'recreated');
341
+ assert.match(created[0] ?? '', /OMX_TEAM_STATE_ROOT='\/team-state-root'/);
342
+ assert.doesNotMatch(created[0] ?? '', /OMX_ROOT=/);
343
+ assert.doesNotMatch(created[0] ?? '', /OMX_STATE_ROOT=/);
344
+ });
131
345
  it('targets the emitting pane window when listing and creating HUD panes', async () => {
132
346
  const listArgs = [];
133
347
  const created = [];
@@ -147,9 +361,138 @@ describe('reconcileHudForPromptSubmit', () => {
147
361
  resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
148
362
  });
149
363
  assert.equal(result.status, 'recreated');
150
- assert.deepEqual(listArgs, ['%leader']);
364
+ assert.deepEqual(listArgs, ['%leader', '%leader']);
151
365
  assert.equal(created[0]?.options?.targetPaneId, '%leader');
152
366
  });
367
+ it('keeps prompt-submit HUD recreation scoped to the emitting pane in multi-pane windows', async () => {
368
+ const created = [];
369
+ const result = await reconcileHudForPromptSubmit('/repo', {
370
+ env: { TMUX: '1', TMUX_PANE: '%right', OMX_SESSION_ID: 'sess-right', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
371
+ listCurrentWindowPanes: () => [
372
+ { paneId: '%left', currentCommand: 'codex', startCommand: 'codex' },
373
+ { paneId: '%right', currentCommand: 'codex', startCommand: 'codex' },
374
+ ],
375
+ createHudWatchPane: (_cwd, _cmd, options) => {
376
+ created.push({ options });
377
+ return '%hud-right';
378
+ },
379
+ resizeTmuxPane: () => true,
380
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
381
+ });
382
+ assert.equal(result.status, 'recreated');
383
+ assert.equal(created[0]?.options?.targetPaneId, '%right');
384
+ assert.equal(Object.hasOwn(created[0]?.options ?? {}, 'fullWidth'), false);
385
+ });
386
+ it('collapses same-owner HUD panes that appear during the create race window', async () => {
387
+ const killed = [];
388
+ const resized = [];
389
+ let listCount = 0;
390
+ const result = await reconcileHudForPromptSubmit('/repo', {
391
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-race', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
392
+ listCurrentWindowPanes: () => {
393
+ listCount += 1;
394
+ if (listCount === 1) {
395
+ return [
396
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
397
+ ];
398
+ }
399
+ return [
400
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
401
+ {
402
+ paneId: '%8',
403
+ currentCommand: 'node',
404
+ startCommand: `exec env OMX_SESSION_ID='sess-race' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' /node /omx.js hud --watch`,
405
+ },
406
+ {
407
+ paneId: '%9',
408
+ currentCommand: 'node',
409
+ startCommand: `exec env OMX_SESSION_ID='sess-race' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' /node /omx.js hud --watch --preset=focused`,
410
+ },
411
+ ];
412
+ },
413
+ createHudWatchPane: () => '%9',
414
+ killTmuxPane: (paneId) => {
415
+ killed.push(paneId);
416
+ return true;
417
+ },
418
+ resizeTmuxPane: (paneId, heightLines) => {
419
+ resized.push({ paneId, heightLines });
420
+ return true;
421
+ },
422
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
423
+ });
424
+ assert.equal(result.status, 'replaced_duplicates');
425
+ assert.equal(result.paneId, '%9');
426
+ assert.equal(result.duplicateCount, 1);
427
+ assert.deepEqual(killed, ['%8']);
428
+ assert.deepEqual(resized, [{ paneId: '%9', heightLines: HUD_TMUX_HEIGHT_LINES }]);
429
+ });
430
+ it('keeps an observed same-owner HUD when the returned create pane is absent from the post-create scan', async () => {
431
+ const killed = [];
432
+ const resized = [];
433
+ let listCount = 0;
434
+ const result = await reconcileHudForPromptSubmit('/repo', {
435
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-race', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
436
+ listCurrentWindowPanes: () => {
437
+ listCount += 1;
438
+ if (listCount === 1)
439
+ return [{ paneId: '%1', currentCommand: 'codex', startCommand: 'codex' }];
440
+ return [
441
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
442
+ {
443
+ paneId: '%8',
444
+ currentCommand: 'node',
445
+ startCommand: `exec env OMX_SESSION_ID='sess-race' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' /node /omx.js hud --watch`,
446
+ },
447
+ ];
448
+ },
449
+ createHudWatchPane: () => '%9',
450
+ killTmuxPane: (paneId) => { killed.push(paneId); return true; },
451
+ resizeTmuxPane: (paneId, heightLines) => { resized.push({ paneId, heightLines }); return true; },
452
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
453
+ });
454
+ assert.equal(result.status, 'recreated');
455
+ assert.equal(result.paneId, '%8');
456
+ assert.equal(result.duplicateCount, 0);
457
+ assert.deepEqual(killed, []);
458
+ assert.deepEqual(resized, [{ paneId: '%8', heightLines: HUD_TMUX_HEIGHT_LINES }]);
459
+ });
460
+ it('kills post-create duplicate HUD panes even when the keeper cannot be resized', async () => {
461
+ const killed = [];
462
+ const registered = [];
463
+ let listCount = 0;
464
+ const result = await reconcileHudForPromptSubmit('/repo', {
465
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-race', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
466
+ listCurrentWindowPanes: () => {
467
+ listCount += 1;
468
+ if (listCount === 1)
469
+ return [{ paneId: '%1', currentCommand: 'codex', startCommand: 'codex' }];
470
+ return [
471
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
472
+ {
473
+ paneId: '%8',
474
+ currentCommand: 'node',
475
+ startCommand: `exec env OMX_SESSION_ID='sess-race' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' /node /omx.js hud --watch`,
476
+ },
477
+ {
478
+ paneId: '%9',
479
+ currentCommand: 'node',
480
+ startCommand: `exec env OMX_SESSION_ID='sess-race' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' /node /omx.js hud --watch`,
481
+ },
482
+ ];
483
+ },
484
+ createHudWatchPane: () => '%9',
485
+ killTmuxPane: (paneId) => { killed.push(paneId); return true; },
486
+ resizeTmuxPane: () => false,
487
+ registerHudResizeHook: (paneId) => { registered.push(paneId); return true; },
488
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
489
+ });
490
+ assert.equal(result.status, 'failed');
491
+ assert.equal(result.paneId, '%9');
492
+ assert.equal(result.duplicateCount, 1);
493
+ assert.deepEqual(killed, ['%8']);
494
+ assert.deepEqual(registered, []);
495
+ });
153
496
  it('kills duplicate HUD panes and reuses one existing pane', async () => {
154
497
  const killed = [];
155
498
  const resized = [];
@@ -190,6 +533,164 @@ describe('reconcileHudForPromptSubmit', () => {
190
533
  assert.deepEqual(resized, [{ paneId: '%2', heightLines: HUD_TMUX_HEIGHT_LINES }]);
191
534
  assert.deepEqual(created, []);
192
535
  });
536
+ it('deduplicates same-leader HUD panes tagged with equivalent owner and canonical session ids', async () => {
537
+ const killed = [];
538
+ const resized = [];
539
+ const created = [];
540
+ const result = await reconcileHudForPromptSubmit('/repo', {
541
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'codex-native-uuid', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
542
+ sessionId: 'omx-owner-abc',
543
+ sessionIds: ['omx-owner-abc', 'codex-native-uuid'],
544
+ listCurrentWindowPanes: () => [
545
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
546
+ {
547
+ paneId: '%2',
548
+ currentCommand: 'node',
549
+ startCommand: `env OMX_SESSION_ID='omx-owner-abc' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' node omx hud --watch`,
550
+ },
551
+ {
552
+ paneId: '%3',
553
+ currentCommand: 'node',
554
+ startCommand: `env OMX_SESSION_ID='codex-native-uuid' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' node omx hud --watch`,
555
+ },
556
+ {
557
+ // Same equivalent session, but its recorded leader is itself a HUD pane;
558
+ // the orphan reaper should remove it before normal same-leader dedupe.
559
+ paneId: '%4',
560
+ currentCommand: 'node',
561
+ startCommand: `env OMX_SESSION_ID='codex-native-uuid' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%4' node omx hud --watch`,
562
+ },
563
+ ],
564
+ killTmuxPane: (paneId) => {
565
+ killed.push(paneId);
566
+ return true;
567
+ },
568
+ createHudWatchPane: (_cwd, cmd) => {
569
+ created.push({ cmd });
570
+ return '%9';
571
+ },
572
+ resizeTmuxPane: (paneId, heightLines) => {
573
+ resized.push({ paneId, heightLines });
574
+ return true;
575
+ },
576
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
577
+ });
578
+ assert.equal(result.status, 'replaced_duplicates');
579
+ assert.equal(result.paneId, '%2');
580
+ assert.equal(result.duplicateCount, 1);
581
+ assert.deepEqual(killed, ['%4', '%3']);
582
+ assert.deepEqual(resized, [{ paneId: '%2', heightLines: HUD_TMUX_HEIGHT_LINES }]);
583
+ assert.deepEqual(created, []);
584
+ });
585
+ it('reuses and deduplicates legacy unowned focused HUD watch panes before recreating', async () => {
586
+ const killed = [];
587
+ const resized = [];
588
+ const created = [];
589
+ const result = await reconcileHudForPromptSubmit('/repo', {
590
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-a', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
591
+ listCurrentWindowPanes: () => [
592
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
593
+ { paneId: '%2', currentCommand: 'node', startCommand: 'node /tmp/bin/omx.js hud --watch --preset=focused' },
594
+ { paneId: '%3', currentCommand: 'node', startCommand: 'node /tmp/bin/omx.js hud --watch --preset=focused' },
595
+ { paneId: '%4', currentCommand: 'node', startCommand: 'node /tmp/bin/omx.js hud --watch --preset=minimal' },
596
+ { paneId: '%5', currentCommand: 'node', startCommand: 'node /tmp/bin/omx.js hud --tmux --preset=focused' },
597
+ ],
598
+ killTmuxPane: (paneId) => { killed.push(paneId); return true; },
599
+ createHudWatchPane: () => { created.push('create'); return '%9'; },
600
+ resizeTmuxPane: (paneId, heightLines) => { resized.push({ paneId, heightLines }); return true; },
601
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
602
+ });
603
+ assert.equal(result.status, 'replaced_duplicates');
604
+ assert.equal(result.paneId, '%2');
605
+ assert.equal(result.duplicateCount, 1);
606
+ assert.deepEqual(killed, ['%3']);
607
+ assert.deepEqual(resized, [{ paneId: '%2', heightLines: HUD_TMUX_HEIGHT_LINES }]);
608
+ assert.deepEqual(created, []);
609
+ });
610
+ it('treats an extra legacy focused pane as stale when an owned HUD already exists', async () => {
611
+ const killed = [];
612
+ const resized = [];
613
+ const result = await reconcileHudForPromptSubmit('/repo', {
614
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-a', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
615
+ listCurrentWindowPanes: () => [
616
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
617
+ {
618
+ paneId: '%2',
619
+ currentCommand: 'node',
620
+ startCommand: `env OMX_SESSION_ID='sess-a' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' node omx hud --watch --preset=focused`,
621
+ },
622
+ { paneId: '%3', currentCommand: 'node', startCommand: 'node /tmp/bin/omx.js hud --watch --preset=focused' },
623
+ ],
624
+ killTmuxPane: (paneId) => { killed.push(paneId); return true; },
625
+ resizeTmuxPane: (paneId, heightLines) => { resized.push({ paneId, heightLines }); return true; },
626
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
627
+ });
628
+ assert.equal(result.status, 'replaced_duplicates');
629
+ assert.equal(result.paneId, '%2');
630
+ assert.equal(result.duplicateCount, 1);
631
+ assert.deepEqual(killed, ['%3']);
632
+ assert.deepEqual(resized, [{ paneId: '%2', heightLines: HUD_TMUX_HEIGHT_LINES }]);
633
+ });
634
+ it('deduplicates legacy focused panes that appear during the prompt-submit create race', async () => {
635
+ const killed = [];
636
+ const resized = [];
637
+ let listCount = 0;
638
+ const result = await reconcileHudForPromptSubmit('/repo', {
639
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-race', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
640
+ listCurrentWindowPanes: () => {
641
+ listCount += 1;
642
+ if (listCount === 1)
643
+ return [{ paneId: '%1', currentCommand: 'codex', startCommand: 'codex' }];
644
+ return [
645
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
646
+ { paneId: '%8', currentCommand: 'node', startCommand: 'node /tmp/bin/omx.js hud --watch --preset=focused' },
647
+ {
648
+ paneId: '%9',
649
+ currentCommand: 'node',
650
+ startCommand: `exec env OMX_SESSION_ID='sess-race' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' /node /omx.js hud --watch --preset=focused`,
651
+ },
652
+ ];
653
+ },
654
+ createHudWatchPane: () => '%9',
655
+ killTmuxPane: (paneId) => { killed.push(paneId); return true; },
656
+ resizeTmuxPane: (paneId, heightLines) => { resized.push({ paneId, heightLines }); return true; },
657
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
658
+ });
659
+ assert.equal(result.status, 'replaced_duplicates');
660
+ assert.equal(result.paneId, '%9');
661
+ assert.equal(result.duplicateCount, 1);
662
+ assert.deepEqual(killed, ['%8']);
663
+ assert.deepEqual(resized, [{ paneId: '%9', heightLines: HUD_TMUX_HEIGHT_LINES }]);
664
+ });
665
+ it('kills existing duplicate HUD panes even when the keeper cannot be resized', async () => {
666
+ const killed = [];
667
+ const registered = [];
668
+ const result = await reconcileHudForPromptSubmit('/repo', {
669
+ env: { TMUX: '1', TMUX_PANE: '%1', OMX_SESSION_ID: 'sess-a', [OMX_TMUX_HUD_OWNER_ENV]: '1' },
670
+ listCurrentWindowPanes: () => [
671
+ { paneId: '%1', currentCommand: 'codex', startCommand: 'codex' },
672
+ {
673
+ paneId: '%8',
674
+ currentCommand: 'node',
675
+ startCommand: `env OMX_SESSION_ID='sess-a' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' node omx hud --watch`,
676
+ },
677
+ {
678
+ paneId: '%9',
679
+ currentCommand: 'node',
680
+ startCommand: `env OMX_SESSION_ID='sess-a' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' node omx hud --watch`,
681
+ },
682
+ ],
683
+ killTmuxPane: (paneId) => { killed.push(paneId); return true; },
684
+ resizeTmuxPane: () => false,
685
+ registerHudResizeHook: (paneId) => { registered.push(paneId); return true; },
686
+ resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
687
+ });
688
+ assert.equal(result.status, 'failed');
689
+ assert.equal(result.paneId, '%8');
690
+ assert.equal(result.duplicateCount, 1);
691
+ assert.deepEqual(killed, ['%9']);
692
+ assert.deepEqual(registered, []);
693
+ });
193
694
  it('does not resize, kill, or reuse another active leader session HUD in the same tmux window', async () => {
194
695
  const killed = [];
195
696
  const resized = [];
@@ -246,6 +747,11 @@ describe('reconcileHudForPromptSubmit', () => {
246
747
  {
247
748
  paneId: '%4',
248
749
  currentCommand: 'node',
750
+ startCommand: `env OMX_SESSION_ID='sess-b' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%1' node omx hud --watch`,
751
+ },
752
+ {
753
+ paneId: '%5',
754
+ currentCommand: 'node',
249
755
  startCommand: `env OMX_SESSION_ID='sess-b' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='%5' node omx hud --watch`,
250
756
  },
251
757
  ],
@@ -384,7 +890,7 @@ describe('reconcileHudForPromptSubmit', () => {
384
890
  assert.equal(result.status, 'resized');
385
891
  assert.deepEqual(resized, [{ paneId: '%2', heightLines: HUD_TMUX_ULTRAGOAL_HEIGHT_LINES }]);
386
892
  });
387
- it('resizes an existing owner-tagged same-leader HUD pane instead of creating a duplicate during prompt revive', async () => {
893
+ it('recreates instead of reusing a leader-only HUD pane when reviving with a canonical session id', async () => {
388
894
  const resized = [];
389
895
  const created = [];
390
896
  const result = await reconcileHudForPromptSubmit('/repo', {
@@ -407,10 +913,10 @@ describe('reconcileHudForPromptSubmit', () => {
407
913
  },
408
914
  resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
409
915
  });
410
- assert.equal(result.status, 'resized');
411
- assert.equal(result.paneId, '%2');
412
- assert.deepEqual(created, []);
413
- assert.deepEqual(resized, [{ paneId: '%2', heightLines: HUD_TMUX_HEIGHT_LINES }]);
916
+ assert.equal(result.status, 'recreated');
917
+ assert.equal(result.paneId, '%9');
918
+ assert.deepEqual(created, ['create']);
919
+ assert.deepEqual(resized, [{ paneId: '%9', heightLines: HUD_TMUX_HEIGHT_LINES }]);
414
920
  });
415
921
  it('deduplicates same-leader HUD panes without creating a new pane when session id is unavailable', async () => {
416
922
  const killed = [];
@@ -476,15 +982,15 @@ describe('reconcileHudForPromptSubmit', () => {
476
982
  },
477
983
  ],
478
984
  resizeTmuxPane: () => true,
479
- registerHudResizeHook: (hudPaneId, currentPaneId, heightLines) => {
480
- registered.push({ hudPaneId, currentPaneId, heightLines });
985
+ registerHudResizeHook: (hudPaneId, leaderPaneId, heightLines) => {
986
+ registered.push({ hudPaneId, leaderPaneId, heightLines });
481
987
  return true;
482
988
  },
483
989
  resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
484
990
  });
485
991
  assert.equal(registered.length, 1);
486
992
  assert.equal(registered[0]?.hudPaneId, '%2');
487
- assert.equal(registered[0]?.currentPaneId, '%1');
993
+ assert.equal(registered[0]?.leaderPaneId, '%1');
488
994
  assert.equal(registered[0]?.heightLines, HUD_TMUX_HEIGHT_LINES);
489
995
  });
490
996
  it('registers client-resized hook scoped from the emitting pane after creating a new HUD pane', async () => {
@@ -496,15 +1002,15 @@ describe('reconcileHudForPromptSubmit', () => {
496
1002
  ],
497
1003
  createHudWatchPane: () => '%9',
498
1004
  resizeTmuxPane: () => true,
499
- registerHudResizeHook: (hudPaneId, currentPaneId, heightLines) => {
500
- registered.push({ hudPaneId, currentPaneId, heightLines });
1005
+ registerHudResizeHook: (hudPaneId, leaderPaneId, heightLines) => {
1006
+ registered.push({ hudPaneId, leaderPaneId, heightLines });
501
1007
  return true;
502
1008
  },
503
1009
  resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
504
1010
  });
505
1011
  assert.equal(registered.length, 1);
506
1012
  assert.equal(registered[0]?.hudPaneId, '%9');
507
- assert.equal(registered[0]?.currentPaneId, '%1');
1013
+ assert.equal(registered[0]?.leaderPaneId, '%1');
508
1014
  assert.equal(registered[0]?.heightLines, HUD_TMUX_HEIGHT_LINES);
509
1015
  });
510
1016
  it('keeps the resize hook on the reused duplicate keeper pane', async () => {
@@ -520,14 +1026,14 @@ describe('reconcileHudForPromptSubmit', () => {
520
1026
  killTmuxPane: () => true,
521
1027
  createHudWatchPane: () => '%9',
522
1028
  resizeTmuxPane: () => true,
523
- unregisterHudResizeHook: (currentPaneId) => { unregistered.push(currentPaneId); return true; },
524
- registerHudResizeHook: (hudPaneId, currentPaneId) => { registered.push({ hudPaneId, currentPaneId }); return true; },
1029
+ unregisterHudResizeHook: (leaderPaneId) => { unregistered.push(leaderPaneId); return true; },
1030
+ registerHudResizeHook: (hudPaneId, leaderPaneId) => { registered.push({ hudPaneId, leaderPaneId }); return true; },
525
1031
  resolveOmxCliEntryPath: () => '/repo/dist/cli/omx.js',
526
1032
  });
527
1033
  assert.deepEqual(unregistered, []);
528
1034
  assert.equal(registered.length, 1);
529
1035
  assert.equal(registered[0]?.hudPaneId, '%2');
530
- assert.equal(registered[0]?.currentPaneId, '%1');
1036
+ assert.equal(registered[0]?.leaderPaneId, '%1');
531
1037
  });
532
1038
  });
533
1039
  //# sourceMappingURL=reconcile.test.js.map