oh-my-codex 0.18.7 → 0.18.9

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 (307) hide show
  1. package/Cargo.lock +12 -12
  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/autopilot/__tests__/fsm.test.js +3 -0
  16. package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
  17. package/dist/autopilot/fsm.js +2 -2
  18. package/dist/autopilot/fsm.js.map +1 -1
  19. package/dist/cli/__tests__/auth.test.js +4 -2
  20. package/dist/cli/__tests__/auth.test.js.map +1 -1
  21. package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
  22. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  25. package/dist/cli/__tests__/index.test.js +98 -6
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/package-bin-contract.test.js +28 -8
  28. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  29. package/dist/cli/__tests__/question.test.js +26 -9
  30. package/dist/cli/__tests__/question.test.js.map +1 -1
  31. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
  32. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  33. package/dist/cli/__tests__/ralph.test.js +14 -0
  34. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  35. package/dist/cli/__tests__/resume.test.js +50 -1
  36. package/dist/cli/__tests__/resume.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
  38. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  39. package/dist/cli/__tests__/setup-refresh.test.js +65 -0
  40. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  41. package/dist/cli/__tests__/state.test.js +21 -0
  42. package/dist/cli/__tests__/state.test.js.map +1 -1
  43. package/dist/cli/__tests__/team.test.js +2 -2
  44. package/dist/cli/__tests__/update.test.js +323 -18
  45. package/dist/cli/__tests__/update.test.js.map +1 -1
  46. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  47. package/dist/cli/doctor.d.ts.map +1 -1
  48. package/dist/cli/doctor.js +8 -1
  49. package/dist/cli/doctor.js.map +1 -1
  50. package/dist/cli/index.d.ts +21 -4
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +143 -28
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/cli/plugin-marketplace.d.ts +14 -2
  55. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  56. package/dist/cli/plugin-marketplace.js +62 -15
  57. package/dist/cli/plugin-marketplace.js.map +1 -1
  58. package/dist/cli/ralph.d.ts.map +1 -1
  59. package/dist/cli/ralph.js +3 -1
  60. package/dist/cli/ralph.js.map +1 -1
  61. package/dist/cli/setup-preferences.d.ts +2 -0
  62. package/dist/cli/setup-preferences.d.ts.map +1 -1
  63. package/dist/cli/setup-preferences.js +4 -0
  64. package/dist/cli/setup-preferences.js.map +1 -1
  65. package/dist/cli/setup.d.ts +3 -0
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +166 -27
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/state.d.ts.map +1 -1
  70. package/dist/cli/state.js +8 -1
  71. package/dist/cli/state.js.map +1 -1
  72. package/dist/cli/tmux-hook.d.ts.map +1 -1
  73. package/dist/cli/tmux-hook.js +16 -0
  74. package/dist/cli/tmux-hook.js.map +1 -1
  75. package/dist/cli/update.d.ts +22 -3
  76. package/dist/cli/update.d.ts.map +1 -1
  77. package/dist/cli/update.js +312 -26
  78. package/dist/cli/update.js.map +1 -1
  79. package/dist/cli/version.d.ts.map +1 -1
  80. package/dist/cli/version.js +5 -9
  81. package/dist/cli/version.js.map +1 -1
  82. package/dist/compat/__tests__/doctor-contract.test.js +12 -1
  83. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  84. package/dist/config/__tests__/generator-notify.test.js +1 -0
  85. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  86. package/dist/config/generator.d.ts +2 -2
  87. package/dist/config/generator.d.ts.map +1 -1
  88. package/dist/config/generator.js +2 -2
  89. package/dist/config/generator.js.map +1 -1
  90. package/dist/config/team-mode.d.ts +12 -0
  91. package/dist/config/team-mode.d.ts.map +1 -0
  92. package/dist/config/team-mode.js +91 -0
  93. package/dist/config/team-mode.js.map +1 -0
  94. package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
  95. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  96. package/dist/hooks/__tests__/code-review-skill-contract.test.js +12 -0
  97. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  98. package/dist/hooks/__tests__/deep-interview-contract.test.js +30 -1
  99. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  100. package/dist/hooks/__tests__/keyword-detector.test.js +423 -3
  101. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  102. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +1 -1
  103. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  104. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
  105. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  106. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
  107. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  108. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
  109. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  110. package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
  111. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  112. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  113. package/dist/hooks/agents-overlay.js +36 -50
  114. package/dist/hooks/agents-overlay.js.map +1 -1
  115. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
  116. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  117. package/dist/hooks/extensibility/plugin-runner.js +17 -21
  118. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  119. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  120. package/dist/hooks/keyword-detector.js +258 -12
  121. package/dist/hooks/keyword-detector.js.map +1 -1
  122. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  123. package/dist/hooks/prompt-guidance-contract.js +6 -0
  124. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  125. package/dist/hooks/session.d.ts +1 -0
  126. package/dist/hooks/session.d.ts.map +1 -1
  127. package/dist/hooks/session.js.map +1 -1
  128. package/dist/hud/__tests__/authority.test.js +435 -32
  129. package/dist/hud/__tests__/authority.test.js.map +1 -1
  130. package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
  131. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  132. package/dist/hud/__tests__/index.test.js +42 -0
  133. package/dist/hud/__tests__/index.test.js.map +1 -1
  134. package/dist/hud/__tests__/reconcile.test.js +642 -15
  135. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  136. package/dist/hud/__tests__/render.test.js +61 -0
  137. package/dist/hud/__tests__/render.test.js.map +1 -1
  138. package/dist/hud/__tests__/state.test.js +160 -4
  139. package/dist/hud/__tests__/state.test.js.map +1 -1
  140. package/dist/hud/__tests__/tmux.test.js +180 -21
  141. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  142. package/dist/hud/authority.d.ts +5 -0
  143. package/dist/hud/authority.d.ts.map +1 -1
  144. package/dist/hud/authority.js +324 -28
  145. package/dist/hud/authority.js.map +1 -1
  146. package/dist/hud/index.d.ts +3 -2
  147. package/dist/hud/index.d.ts.map +1 -1
  148. package/dist/hud/index.js +42 -19
  149. package/dist/hud/index.js.map +1 -1
  150. package/dist/hud/reconcile.d.ts +3 -3
  151. package/dist/hud/reconcile.d.ts.map +1 -1
  152. package/dist/hud/reconcile.js +128 -19
  153. package/dist/hud/reconcile.js.map +1 -1
  154. package/dist/hud/render.d.ts.map +1 -1
  155. package/dist/hud/render.js +35 -0
  156. package/dist/hud/render.js.map +1 -1
  157. package/dist/hud/state.d.ts.map +1 -1
  158. package/dist/hud/state.js +65 -80
  159. package/dist/hud/state.js.map +1 -1
  160. package/dist/hud/tmux.d.ts +24 -6
  161. package/dist/hud/tmux.d.ts.map +1 -1
  162. package/dist/hud/tmux.js +136 -38
  163. package/dist/hud/tmux.js.map +1 -1
  164. package/dist/hud/types.d.ts +11 -0
  165. package/dist/hud/types.d.ts.map +1 -1
  166. package/dist/hud/types.js.map +1 -1
  167. package/dist/mcp/__tests__/state-paths.test.js +71 -1
  168. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  169. package/dist/mcp/state-paths.d.ts +32 -0
  170. package/dist/mcp/state-paths.d.ts.map +1 -1
  171. package/dist/mcp/state-paths.js +113 -17
  172. package/dist/mcp/state-paths.js.map +1 -1
  173. package/dist/mcp/state-server.d.ts +4 -4
  174. package/dist/question/__tests__/renderer.test.js +566 -1
  175. package/dist/question/__tests__/renderer.test.js.map +1 -1
  176. package/dist/question/renderer.d.ts +9 -1
  177. package/dist/question/renderer.d.ts.map +1 -1
  178. package/dist/question/renderer.js +246 -70
  179. package/dist/question/renderer.js.map +1 -1
  180. package/dist/scripts/__tests__/codex-native-hook.test.js +837 -101
  181. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  182. package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
  183. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  184. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
  185. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
  186. package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
  187. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
  188. package/dist/scripts/__tests__/run-test-files.test.js +74 -0
  189. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  190. package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
  191. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  192. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  193. package/dist/scripts/codex-native-hook.js +107 -39
  194. package/dist/scripts/codex-native-hook.js.map +1 -1
  195. package/dist/scripts/eval/eval-parity-smoke.js +1 -1
  196. package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
  197. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  198. package/dist/scripts/notify-hook/auto-nudge.js +3 -1
  199. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  200. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  201. package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
  202. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  203. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/state-io.js +62 -38
  205. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  206. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  207. package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
  208. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  209. package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
  210. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  211. package/dist/scripts/notify-hook/tmux-injection.js +24 -18
  212. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  213. package/dist/scripts/notify-hook.js +75 -11
  214. package/dist/scripts/notify-hook.js.map +1 -1
  215. package/dist/scripts/run-test-files.js +193 -22
  216. package/dist/scripts/run-test-files.js.map +1 -1
  217. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  218. package/dist/scripts/sync-plugin-mirror.js +61 -3
  219. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  220. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  221. package/dist/scripts/verify-native-agents.js +58 -1
  222. package/dist/scripts/verify-native-agents.js.map +1 -1
  223. package/dist/state/__tests__/operations.test.js +113 -0
  224. package/dist/state/__tests__/operations.test.js.map +1 -1
  225. package/dist/state/__tests__/skill-active.test.js +3 -16
  226. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  227. package/dist/state/__tests__/workflow-transition.test.js +25 -0
  228. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  229. package/dist/state/operations.d.ts.map +1 -1
  230. package/dist/state/operations.js +57 -2
  231. package/dist/state/operations.js.map +1 -1
  232. package/dist/state/skill-active.d.ts.map +1 -1
  233. package/dist/state/skill-active.js +7 -39
  234. package/dist/state/skill-active.js.map +1 -1
  235. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  236. package/dist/state/workflow-transition-reconcile.js +10 -14
  237. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  238. package/dist/team/__tests__/runtime.test.js +1 -1
  239. package/dist/team/__tests__/runtime.test.js.map +1 -1
  240. package/dist/team/__tests__/scaling.test.js +9 -4
  241. package/dist/team/__tests__/scaling.test.js.map +1 -1
  242. package/dist/team/__tests__/tmux-session.test.js +195 -2
  243. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  244. package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
  245. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  246. package/dist/team/scaling.d.ts.map +1 -1
  247. package/dist/team/scaling.js +3 -2
  248. package/dist/team/scaling.js.map +1 -1
  249. package/dist/team/tmux-session.d.ts +2 -0
  250. package/dist/team/tmux-session.d.ts.map +1 -1
  251. package/dist/team/tmux-session.js +142 -12
  252. package/dist/team/tmux-session.js.map +1 -1
  253. package/dist/utils/__tests__/platform-command.test.js +16 -1
  254. package/dist/utils/__tests__/platform-command.test.js.map +1 -1
  255. package/dist/utils/__tests__/version.test.d.ts +2 -0
  256. package/dist/utils/__tests__/version.test.d.ts.map +1 -0
  257. package/dist/utils/__tests__/version.test.js +51 -0
  258. package/dist/utils/__tests__/version.test.js.map +1 -0
  259. package/dist/utils/paths.d.ts +8 -1
  260. package/dist/utils/paths.d.ts.map +1 -1
  261. package/dist/utils/paths.js +16 -4
  262. package/dist/utils/paths.js.map +1 -1
  263. package/dist/utils/platform-command.d.ts +9 -0
  264. package/dist/utils/platform-command.d.ts.map +1 -1
  265. package/dist/utils/platform-command.js +15 -0
  266. package/dist/utils/platform-command.js.map +1 -1
  267. package/dist/utils/version.d.ts +7 -0
  268. package/dist/utils/version.d.ts.map +1 -0
  269. package/dist/utils/version.js +67 -0
  270. package/dist/utils/version.js.map +1 -0
  271. package/dist/verification/__tests__/ci-rust-gates.test.js +89 -1
  272. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  273. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +16 -2
  274. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  275. package/package.json +11 -10
  276. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  277. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
  278. package/plugins/oh-my-codex/hooks/hooks.json +1 -2
  279. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -1
  280. package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
  281. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +51 -11
  282. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
  283. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
  284. package/skills/autopilot/SKILL.md +3 -1
  285. package/skills/code-review/SKILL.md +7 -7
  286. package/skills/deep-interview/SKILL.md +51 -11
  287. package/skills/ralph/SKILL.md +22 -22
  288. package/skills/ultraqa/SKILL.md +9 -0
  289. package/src/scripts/__tests__/codex-native-hook.test.ts +946 -98
  290. package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
  291. package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
  292. package/src/scripts/__tests__/run-test-files.test.ts +102 -0
  293. package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
  294. package/src/scripts/codex-native-hook.ts +123 -34
  295. package/src/scripts/demo-team-e2e.sh +10 -7
  296. package/src/scripts/eval/eval-parity-smoke.ts +1 -1
  297. package/src/scripts/notify-hook/auto-nudge.ts +3 -1
  298. package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
  299. package/src/scripts/notify-hook/state-io.ts +75 -37
  300. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
  301. package/src/scripts/notify-hook/tmux-injection.ts +35 -19
  302. package/src/scripts/notify-hook.ts +91 -4
  303. package/src/scripts/prepare-build.js +83 -0
  304. package/src/scripts/run-test-files.ts +192 -22
  305. package/src/scripts/sync-plugin-mirror.ts +98 -9
  306. package/src/scripts/verify-native-agents.ts +65 -1
  307. package/src/scripts/postinstall-bootstrap.js +0 -23
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { describe, it } from 'node:test';
6
- import { closeQuestionRenderer, computeAdaptiveQuestionPaneHeight, formatQuestionAnswerForInjection, formatQuestionAnswersForInjection, injectQuestionAnswerToPane, findLiveQuestionsForSession, injectQuestionAnswersToPane, launchQuestionRenderer, resolveQuestionRendererStrategy, supersedeLiveQuestionsForSession, } from '../renderer.js';
6
+ import { buildQuestionUiTmuxArgs, closeQuestionRenderer, computeAdaptiveQuestionPaneHeight, formatQuestionAnswerForInjection, formatQuestionAnswersForInjection, injectQuestionAnswerToPane, findLiveQuestionsForSession, injectQuestionAnswersToPane, launchQuestionRenderer, resolveQuestionRendererStrategy, estimateQuestionRenderFootprint, shouldOpenQuestionInNewWindow, supersedeLiveQuestionsForSession, } from '../renderer.js';
7
7
  import { buildSendPaneArgvs } from '../../notifications/tmux-detector.js';
8
8
  describe('resolveQuestionRendererStrategy', () => {
9
9
  it('prefers inside-tmux when TMUX is present', () => {
@@ -90,6 +90,185 @@ describe('adaptive question pane sizing', () => {
90
90
  assert.equal(computeAdaptiveQuestionPaneHeight(9, 20), 7);
91
91
  });
92
92
  });
93
+ describe('question window topology selection', () => {
94
+ it('switches to a new tmux window only after the split height budget is exceeded', () => {
95
+ assert.equal(shouldOpenQuestionInNewWindow(20, 18), false);
96
+ assert.equal(shouldOpenQuestionInNewWindow(20, 19), true);
97
+ assert.equal(shouldOpenQuestionInNewWindow(5, 3), false);
98
+ assert.equal(shouldOpenQuestionInNewWindow(5, 4), true);
99
+ });
100
+ it('counts wrapped question text more conservatively in narrow panes', () => {
101
+ const record = {
102
+ kind: 'omx.question/v1',
103
+ question_id: 'question-1',
104
+ created_at: '2026-05-01T10:08:52.523Z',
105
+ updated_at: '2026-05-01T10:08:52.523Z',
106
+ status: 'pending',
107
+ question: 'x'.repeat(180),
108
+ options: [{
109
+ label: 'Only option',
110
+ value: 'only',
111
+ description: 'y'.repeat(140),
112
+ }],
113
+ allow_other: false,
114
+ multi_select: false,
115
+ type: 'single-answerable',
116
+ source: 'deep-interview',
117
+ questions: [{
118
+ id: 'q-1',
119
+ question: 'x'.repeat(180),
120
+ options: [{
121
+ label: 'Only option',
122
+ value: 'only',
123
+ description: 'y'.repeat(140),
124
+ }],
125
+ allow_other: false,
126
+ multi_select: false,
127
+ type: 'single-answerable',
128
+ }],
129
+ };
130
+ assert.ok(estimateQuestionRenderFootprint(record, 20) > estimateQuestionRenderFootprint(record, 80));
131
+ });
132
+ it('sizes multi-question records from the largest visible screen rather than summing every question', () => {
133
+ const longQuestion = {
134
+ id: 'q-2',
135
+ question: 'x'.repeat(220),
136
+ options: [{
137
+ label: 'Only option',
138
+ value: 'only',
139
+ description: 'y'.repeat(180),
140
+ }],
141
+ allow_other: false,
142
+ multi_select: false,
143
+ type: 'single-answerable',
144
+ };
145
+ const shortQuestion = {
146
+ id: 'q-1',
147
+ question: 'Short question?',
148
+ options: [{
149
+ label: 'Only option',
150
+ value: 'only',
151
+ description: 'Short description.',
152
+ }],
153
+ allow_other: false,
154
+ multi_select: false,
155
+ type: 'single-answerable',
156
+ };
157
+ const shared = {
158
+ kind: 'omx.question/v1',
159
+ question_id: 'question-1',
160
+ created_at: '2026-05-01T10:08:52.523Z',
161
+ updated_at: '2026-05-01T10:08:52.523Z',
162
+ status: 'pending',
163
+ allow_other: false,
164
+ multi_select: false,
165
+ type: 'single-answerable',
166
+ source: 'deep-interview',
167
+ };
168
+ const multiQuestionRecord = {
169
+ ...shared,
170
+ questions: [shortQuestion, longQuestion],
171
+ };
172
+ const shortRecord = {
173
+ ...shared,
174
+ questions: [shortQuestion],
175
+ };
176
+ const longRecord = {
177
+ ...shared,
178
+ questions: [longQuestion],
179
+ };
180
+ const multiFootprint = estimateQuestionRenderFootprint(multiQuestionRecord, 20);
181
+ const shortFootprint = estimateQuestionRenderFootprint(shortRecord, 20);
182
+ const longFootprint = estimateQuestionRenderFootprint(longRecord, 20);
183
+ assert.ok(multiFootprint >= longFootprint);
184
+ assert.ok(multiFootprint < shortFootprint + longFootprint);
185
+ });
186
+ it('includes the review screen when sizing multi-question records', () => {
187
+ const shortQuestion = {
188
+ id: 'q-1',
189
+ question: 'First short question?',
190
+ options: [{
191
+ label: 'Only option',
192
+ value: 'only',
193
+ description: 'Short description.',
194
+ }],
195
+ allow_other: false,
196
+ multi_select: false,
197
+ type: 'single-answerable',
198
+ };
199
+ const shortSecondQuestion = {
200
+ id: 'q-2',
201
+ question: 'Second short question?',
202
+ options: [{
203
+ label: 'Only option',
204
+ value: 'only',
205
+ description: 'Short description.',
206
+ }],
207
+ allow_other: false,
208
+ multi_select: false,
209
+ type: 'single-answerable',
210
+ };
211
+ const shortThirdQuestion = {
212
+ id: 'q-3',
213
+ question: 'Third short question?',
214
+ options: [{
215
+ label: 'Only option',
216
+ value: 'only',
217
+ description: 'Short description.',
218
+ }],
219
+ allow_other: false,
220
+ multi_select: false,
221
+ type: 'single-answerable',
222
+ };
223
+ const shared = {
224
+ kind: 'omx.question/v1',
225
+ question_id: 'question-1',
226
+ created_at: '2026-05-01T10:08:52.523Z',
227
+ updated_at: '2026-05-01T10:08:52.523Z',
228
+ status: 'pending',
229
+ allow_other: false,
230
+ multi_select: false,
231
+ type: 'single-answerable',
232
+ source: 'deep-interview',
233
+ };
234
+ const singleScreenRecord = {
235
+ ...shared,
236
+ questions: [shortQuestion],
237
+ };
238
+ const reviewScreenRecord = {
239
+ ...shared,
240
+ questions: [shortQuestion, shortSecondQuestion, shortThirdQuestion],
241
+ };
242
+ assert.ok(estimateQuestionRenderFootprint(reviewScreenRecord, 80) > estimateQuestionRenderFootprint(singleScreenRecord, 80));
243
+ });
244
+ it('sizes review screens from selected answers when multi-select summaries wrap', () => {
245
+ const questions = Array.from({ length: 5 }, (_, index) => ({
246
+ id: `q-${index + 1}`,
247
+ question: `Question ${index + 1}?`,
248
+ options: [
249
+ { label: 'Alpha option', value: 'alpha' },
250
+ { label: 'Beta option', value: 'beta' },
251
+ { label: 'Gamma option', value: 'gamma' },
252
+ ],
253
+ allow_other: false,
254
+ multi_select: true,
255
+ type: 'multi-answerable',
256
+ }));
257
+ const record = {
258
+ kind: 'omx.question/v1',
259
+ question_id: 'question-1',
260
+ created_at: '2026-05-01T10:08:52.523Z',
261
+ updated_at: '2026-05-01T10:08:52.523Z',
262
+ status: 'pending',
263
+ allow_other: false,
264
+ multi_select: true,
265
+ type: 'multi-answerable',
266
+ source: 'deep-interview',
267
+ questions,
268
+ };
269
+ assert.equal(shouldOpenQuestionInNewWindow(20, estimateQuestionRenderFootprint(record, 20)), true);
270
+ });
271
+ });
93
272
  describe('launchQuestionRenderer', () => {
94
273
  it('fails before building UI argv or invoking tmux when no visible renderer is available', () => {
95
274
  const calls = [];
@@ -133,6 +312,8 @@ describe('launchQuestionRenderer', () => {
133
312
  strategy: 'inside-tmux',
134
313
  execTmux: (args) => {
135
314
  calls.push(args);
315
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
316
+ return '40\n';
136
317
  if (args[0] === 'display-message')
137
318
  return '1\n';
138
319
  if (args[0] === 'split-window')
@@ -168,6 +349,239 @@ describe('launchQuestionRenderer', () => {
168
349
  assert.ok(splitCall.includes('OMX_QUESTION_RETURN_TRANSPORT=tmux-send-keys'));
169
350
  assert.ok(calls.some((call) => call.join(' ') === 'list-panes -t %42 -F #{pane_dead}\t#{pane_id}'));
170
351
  });
352
+ it('opens a new tmux window when the current pane is too short for the question frame', () => {
353
+ const cwd = mkdtempSync(join(tmpdir(), 'omx-question-renderer-new-window-'));
354
+ try {
355
+ const stateDir = join(cwd, '.omx', 'state', 'sessions', 's1', 'questions');
356
+ mkdirSync(stateDir, { recursive: true });
357
+ const recordPath = join(stateDir, 'question-1.json');
358
+ writeFileSync(recordPath, JSON.stringify({
359
+ kind: 'omx.question/v1',
360
+ question_id: 'question-1',
361
+ created_at: '2026-05-01T10:08:52.523Z',
362
+ updated_at: '2026-05-01T10:08:52.523Z',
363
+ status: 'pending',
364
+ question: 'Round 1 | Target: definition-boundary | Ambiguity: 42%\n\nСамая важная неоднозначность: что именно считать системной метрикой скорости для TTS / Image / Voice, чтобы прогресс-бар и карточки не врали оператору?',
365
+ options: [
366
+ { label: 'Stage timings', value: 'stage-timings', description: 'Считать отдельно реальные этапы: TTS synthesis, image/still generation, voice/video final generation; для каждого нужны stage timestamps/metadata.' },
367
+ { label: 'Wall-clock by artifact', value: 'wall-clock-by-artifact', description: 'Брать общий wall-clock от createdAt до completed и нормализовать по типу результата: 1 image / N sec, 1s video / N sec.' },
368
+ { label: 'Hybrid recommended', value: 'hybrid', description: 'Сначала использовать wall-clock fallback, но добавлять stage timings для новых генераций, когда этапы можно инструментировать.' },
369
+ ],
370
+ allow_other: true,
371
+ other_label: 'Other',
372
+ multi_select: false,
373
+ type: 'single-answerable',
374
+ questions: [{
375
+ id: 'q-1',
376
+ question: 'Round 1 | Target: definition-boundary | Ambiguity: 42%\n\nСамая важная неоднозначность: что именно считать системной метрикой скорости для TTS / Image / Voice, чтобы прогресс-бар и карточки не врали оператору?',
377
+ options: [
378
+ { label: 'Stage timings', value: 'stage-timings', description: 'Считать отдельно реальные этапы: TTS synthesis, image/still generation, voice/video final generation; для каждого нужны stage timestamps/metadata.' },
379
+ { label: 'Wall-clock by artifact', value: 'wall-clock-by-artifact', description: 'Брать общий wall-clock от createdAt до completed и нормализовать по типу результата: 1 image / N sec, 1s video / N sec.' },
380
+ { label: 'Hybrid recommended', value: 'hybrid', description: 'Сначала использовать wall-clock fallback, но добавлять stage timings для новых генераций, когда этапы можно инструментировать.' },
381
+ ],
382
+ allow_other: true,
383
+ other_label: 'Other',
384
+ multi_select: false,
385
+ type: 'single-answerable',
386
+ }],
387
+ source: 'deep-interview',
388
+ }, null, 2));
389
+ const calls = [];
390
+ const result = launchQuestionRenderer({
391
+ cwd,
392
+ recordPath,
393
+ sessionId: 's1',
394
+ env: { TMUX: '/tmp/tmux-demo', TMUX_PANE: '%11' },
395
+ }, {
396
+ strategy: 'inside-tmux',
397
+ execTmux: (args) => {
398
+ calls.push(args);
399
+ if (args[0] === 'display-message' && args.includes('#{session_attached}'))
400
+ return '1\n';
401
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
402
+ return '5\n';
403
+ if (args[0] === 'display-message' && args.includes('#{session_id}'))
404
+ return '$1\n';
405
+ if (args[0] === 'new-window')
406
+ return '%42\n';
407
+ if (args[0] === 'list-panes' && args[2] === '%42')
408
+ return '0\t%42\n';
409
+ return '';
410
+ },
411
+ sleepSync: () => { },
412
+ });
413
+ assert.equal(result.renderer, 'tmux-pane');
414
+ assert.equal(result.target, '%42');
415
+ assert.equal(result.return_target, '%11');
416
+ assert.equal(result.return_transport, 'tmux-send-keys');
417
+ const newWindowCall = calls.find((call) => call[0] === 'new-window');
418
+ assert.ok(newWindowCall);
419
+ const targetIndex = newWindowCall.indexOf('-t');
420
+ assert.notEqual(targetIndex, -1);
421
+ assert.deepEqual(newWindowCall.slice(targetIndex, targetIndex + 2), ['-t', '$1']);
422
+ assert.equal(calls.some((call) => call[0] === 'split-window'), false);
423
+ assert.equal(calls.some((call) => call[0] === 'display-message' && call.includes('#{session_id}')), true);
424
+ }
425
+ finally {
426
+ rmSync(cwd, { recursive: true });
427
+ }
428
+ });
429
+ it('opens a new tmux window when wrapped content would exceed the split budget in a narrow pane', () => {
430
+ const cwd = mkdtempSync(join(tmpdir(), 'omx-question-renderer-wrapped-window-'));
431
+ try {
432
+ const stateDir = join(cwd, '.omx', 'state', 'sessions', 's1', 'questions');
433
+ mkdirSync(stateDir, { recursive: true });
434
+ const recordPath = join(stateDir, 'question-1.json');
435
+ writeFileSync(recordPath, JSON.stringify({
436
+ kind: 'omx.question/v1',
437
+ question_id: 'question-1',
438
+ created_at: '2026-05-01T10:08:52.523Z',
439
+ updated_at: '2026-05-01T10:08:52.523Z',
440
+ status: 'pending',
441
+ question: 'x'.repeat(220),
442
+ options: [{
443
+ label: 'Only option',
444
+ value: 'only',
445
+ description: 'y'.repeat(180),
446
+ }],
447
+ allow_other: false,
448
+ multi_select: false,
449
+ type: 'single-answerable',
450
+ source: 'deep-interview',
451
+ questions: [{
452
+ id: 'q-1',
453
+ question: 'x'.repeat(220),
454
+ options: [{
455
+ label: 'Only option',
456
+ value: 'only',
457
+ description: 'y'.repeat(180),
458
+ }],
459
+ allow_other: false,
460
+ multi_select: false,
461
+ type: 'single-answerable',
462
+ }],
463
+ }, null, 2));
464
+ const calls = [];
465
+ const result = launchQuestionRenderer({
466
+ cwd,
467
+ recordPath,
468
+ sessionId: 's1',
469
+ env: { TMUX: '/tmp/tmux-demo', TMUX_PANE: '%11' },
470
+ }, {
471
+ strategy: 'inside-tmux',
472
+ execTmux: (args) => {
473
+ calls.push(args);
474
+ if (args[0] === 'display-message' && args.includes('#{session_attached}'))
475
+ return '1\n';
476
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
477
+ return '20\n';
478
+ if (args[0] === 'display-message' && args.includes('#{pane_width}'))
479
+ return '20\n';
480
+ if (args[0] === 'display-message' && args.includes('#{session_id}'))
481
+ return '$1\n';
482
+ if (args[0] === 'new-window')
483
+ return '%99\n';
484
+ if (args[0] === 'list-panes' && args[2] === '%99')
485
+ return '0\t%99\n';
486
+ return '';
487
+ },
488
+ sleepSync: () => { },
489
+ });
490
+ assert.equal(result.renderer, 'tmux-pane');
491
+ assert.equal(result.target, '%99');
492
+ assert.equal(result.return_target, '%11');
493
+ assert.equal(result.return_transport, 'tmux-send-keys');
494
+ const newWindowCall = calls.find((call) => call[0] === 'new-window');
495
+ assert.ok(newWindowCall);
496
+ const targetIndex = newWindowCall.indexOf('-t');
497
+ assert.notEqual(targetIndex, -1);
498
+ assert.deepEqual(newWindowCall.slice(targetIndex, targetIndex + 2), ['-t', '$1']);
499
+ assert.equal(calls.some((call) => call[0] === 'split-window'), false);
500
+ }
501
+ finally {
502
+ rmSync(cwd, { recursive: true });
503
+ }
504
+ });
505
+ it('falls back to the default tmux width when the width probe fails', () => {
506
+ const cwd = mkdtempSync(join(tmpdir(), 'omx-question-renderer-width-fallback-'));
507
+ try {
508
+ const stateDir = join(cwd, '.omx', 'state', 'sessions', 's1', 'questions');
509
+ mkdirSync(stateDir, { recursive: true });
510
+ const recordPath = join(stateDir, 'question-1.json');
511
+ writeFileSync(recordPath, JSON.stringify({
512
+ kind: 'omx.question/v1',
513
+ question_id: 'question-1',
514
+ created_at: '2026-05-01T10:08:52.523Z',
515
+ updated_at: '2026-05-01T10:08:52.523Z',
516
+ status: 'pending',
517
+ question: 'x'.repeat(2000),
518
+ options: [{
519
+ label: 'Only option',
520
+ value: 'only',
521
+ description: 'y'.repeat(1000),
522
+ }],
523
+ allow_other: false,
524
+ multi_select: false,
525
+ type: 'single-answerable',
526
+ source: 'deep-interview',
527
+ questions: [{
528
+ id: 'q-1',
529
+ question: 'x'.repeat(2000),
530
+ options: [{
531
+ label: 'Only option',
532
+ value: 'only',
533
+ description: 'y'.repeat(1000),
534
+ }],
535
+ allow_other: false,
536
+ multi_select: false,
537
+ type: 'single-answerable',
538
+ }],
539
+ }, null, 2));
540
+ const calls = [];
541
+ const result = launchQuestionRenderer({
542
+ cwd,
543
+ recordPath,
544
+ sessionId: 's1',
545
+ env: { TMUX: '/tmp/tmux-demo', TMUX_PANE: '%11' },
546
+ }, {
547
+ strategy: 'inside-tmux',
548
+ execTmux: (args) => {
549
+ calls.push(args);
550
+ if (args[0] === 'display-message' && args.includes('#{session_attached}'))
551
+ return '1\n';
552
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
553
+ return '20\n';
554
+ if (args[0] === 'display-message' && args.includes('#{pane_width}') && args.includes('-t'))
555
+ throw new Error('width query failed');
556
+ if (args[0] === 'display-message' && args.includes('#{pane_width}') && !args.includes('-t'))
557
+ return '3\n';
558
+ if (args[0] === 'display-message' && args.includes('#{session_id}'))
559
+ return '$1\n';
560
+ if (args[0] === 'new-window')
561
+ return '%88\n';
562
+ if (args[0] === 'list-panes' && args[2] === '%88')
563
+ return '0\t%88\n';
564
+ return '';
565
+ },
566
+ sleepSync: () => { },
567
+ });
568
+ assert.equal(result.renderer, 'tmux-pane');
569
+ assert.equal(result.target, '%88');
570
+ assert.equal(result.return_target, '%11');
571
+ assert.equal(result.return_transport, 'tmux-send-keys');
572
+ const newWindowCall = calls.find((call) => call[0] === 'new-window');
573
+ assert.ok(newWindowCall);
574
+ const targetIndex = newWindowCall.indexOf('-t');
575
+ assert.notEqual(targetIndex, -1);
576
+ assert.deepEqual(newWindowCall.slice(targetIndex, targetIndex + 2), ['-t', '$1']);
577
+ assert.equal(calls.some((call) => call[0] === 'display-message' && call.includes('#{pane_height}')), false);
578
+ assert.equal(calls.some((call) => call[0] === 'display-message' && call.includes('#{pane_width}') && !call.includes('-t')), false);
579
+ assert.equal(calls.some((call) => call[0] === 'split-window'), false);
580
+ }
581
+ finally {
582
+ rmSync(cwd, { recursive: true });
583
+ }
584
+ });
171
585
  it('targets the explicit leader pane even when the caller is already inside tmux', () => {
172
586
  const calls = [];
173
587
  const result = launchQuestionRenderer({
@@ -239,6 +653,10 @@ describe('launchQuestionRenderer', () => {
239
653
  }, {
240
654
  execTmux: (args) => {
241
655
  calls.push(args);
656
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
657
+ return '40\n';
658
+ if (args[0] === 'display-message' && args.includes('#{pane_width}'))
659
+ return '80\n';
242
660
  if (args[0] === 'split-window')
243
661
  return '%78\n';
244
662
  if (args[0] === 'list-panes')
@@ -321,6 +739,10 @@ describe('launchQuestionRenderer', () => {
321
739
  }, {
322
740
  execTmux: (args) => {
323
741
  calls.push(args);
742
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
743
+ return '40\n';
744
+ if (args[0] === 'display-message' && args.includes('#{pane_width}'))
745
+ return '80\n';
324
746
  if (args[0] === 'split-window')
325
747
  return '%92\n';
326
748
  if (args[0] === 'list-panes')
@@ -355,6 +777,8 @@ describe('launchQuestionRenderer', () => {
355
777
  strategy: 'inside-tmux',
356
778
  execTmux: (args) => {
357
779
  calls.push(args);
780
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
781
+ return '40\n';
358
782
  if (args[0] === 'display-message')
359
783
  return '1\n';
360
784
  if (args[0] === 'split-window')
@@ -419,6 +843,10 @@ describe('launchQuestionRenderer', () => {
419
843
  strategy: 'inside-tmux',
420
844
  execTmux: (args) => {
421
845
  calls.push(args);
846
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
847
+ return '40\n';
848
+ if (args[0] === 'display-message' && args.includes('#{pane_width}'))
849
+ return '80\n';
422
850
  if (args[0] === 'display-message')
423
851
  return '1\n';
424
852
  if (args[0] === 'split-window')
@@ -464,6 +892,10 @@ describe('launchQuestionRenderer', () => {
464
892
  }, {
465
893
  strategy: 'inside-tmux',
466
894
  execTmux: (args) => {
895
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
896
+ return '40\n';
897
+ if (args[0] === 'display-message' && args.includes('#{pane_width}'))
898
+ return '80\n';
467
899
  if (args[0] === 'display-message')
468
900
  return '1\n';
469
901
  if (args[0] === 'split-window')
@@ -491,6 +923,8 @@ describe('launchQuestionRenderer', () => {
491
923
  strategy: 'inside-tmux',
492
924
  execTmux: (args) => {
493
925
  calls.push(args);
926
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
927
+ return '40\n';
494
928
  if (args[0] === 'display-message')
495
929
  return '1\n';
496
930
  if (args[0] === 'split-window')
@@ -531,6 +965,8 @@ describe('launchQuestionRenderer', () => {
531
965
  strategy: 'inside-tmux',
532
966
  execTmux: (args) => {
533
967
  calls.push(args);
968
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
969
+ return '40\n';
534
970
  if (args[0] === 'display-message')
535
971
  return '1\n';
536
972
  if (args[0] === 'split-window') {
@@ -628,6 +1064,8 @@ describe('launchQuestionRenderer', () => {
628
1064
  strategy: 'inside-tmux',
629
1065
  execTmux: (args) => {
630
1066
  calls.push(args);
1067
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
1068
+ return '40\n';
631
1069
  if (args[0] === 'display-message')
632
1070
  return '1\n';
633
1071
  if (args[0] === 'split-window')
@@ -838,6 +1276,8 @@ describe('question renderer in-flight dedupe', () => {
838
1276
  return '1\n';
839
1277
  if (args[0] === 'display-message' && args.includes('#{pane_height}'))
840
1278
  return '40\n';
1279
+ if (args[0] === 'display-message' && args.includes('#{pane_width}'))
1280
+ return '80\n';
841
1281
  if (args[0] === 'list-panes' && args[2] === '%41')
842
1282
  return '0\t%41\n';
843
1283
  if (args[0] === 'kill-pane')
@@ -893,4 +1333,129 @@ describe('question renderer in-flight dedupe', () => {
893
1333
  }
894
1334
  });
895
1335
  });
1336
+ describe('buildQuestionUiTmuxArgs', () => {
1337
+ const recordPath = '/repo/.omx/state/sessions/s1/questions/question-1.json';
1338
+ it('passes env via tmux -e flags on real tmux (no cmux)', () => {
1339
+ const args = buildQuestionUiTmuxArgs(recordPath, {
1340
+ cwd: '/repo',
1341
+ sessionId: 's1',
1342
+ returnTarget: '%11',
1343
+ underCmux: false,
1344
+ });
1345
+ assert.ok(args.includes('-e'));
1346
+ assert.ok(args.includes('OMX_SESSION_ID=s1'));
1347
+ assert.ok(args.includes('OMX_QUESTION_RETURN_TARGET=%11'));
1348
+ assert.ok(args.includes('OMX_QUESTION_RETURN_TRANSPORT=tmux-send-keys'));
1349
+ // tmux execs the command argv directly, so command tokens stay raw/unquoted.
1350
+ assert.ok(args.includes(process.execPath));
1351
+ assert.equal(args.includes('export'), false);
1352
+ assert.equal(args.includes('&&'), false);
1353
+ assert.equal(args.some((token) => /^'.*'$/.test(token)), false);
1354
+ });
1355
+ it('delivers env via a single shell-neutral env-prefixed command and never emits a bare -e under cmux', () => {
1356
+ const args = buildQuestionUiTmuxArgs(recordPath, {
1357
+ cwd: '/repo',
1358
+ sessionId: 's1',
1359
+ returnTarget: '%11',
1360
+ underCmux: true,
1361
+ });
1362
+ // A single shell-command argument: stays correct on both the cmux shim and a
1363
+ // real tmux that inherits cmux env vars (single arg -> run via the shell).
1364
+ assert.equal(args.length, 1);
1365
+ const command = args[0];
1366
+ // `env` keeps it shell-neutral (works in fish/zsh/sh); no POSIX-only `export`/`&&`.
1367
+ assert.match(command, /^env /);
1368
+ assert.equal(command.includes('export'), false);
1369
+ assert.equal(command.includes('&&'), false);
1370
+ assert.equal(command.startsWith('-e'), false);
1371
+ assert.equal(command.includes(' -e '), false);
1372
+ assert.ok(command.includes("OMX_SESSION_ID='s1'"));
1373
+ assert.ok(command.includes("OMX_QUESTION_RETURN_TARGET='%11'"));
1374
+ assert.ok(command.includes("OMX_QUESTION_RETURN_TRANSPORT='tmux-send-keys'"));
1375
+ // The executable env runs is the real command (quoted node), never `-e`.
1376
+ assert.ok(command.includes(`'tmux-send-keys' '${process.execPath}' `));
1377
+ assert.ok(command.endsWith(`'${recordPath}'`));
1378
+ });
1379
+ it('single-quotes values containing =, % and spaces so they survive the cmux shell command', () => {
1380
+ const trickyReturnTarget = 'pane=%5 with spaces';
1381
+ const trickySessionId = 'sess=a%b c';
1382
+ const args = buildQuestionUiTmuxArgs(recordPath, {
1383
+ cwd: '/repo',
1384
+ sessionId: trickySessionId,
1385
+ returnTarget: trickyReturnTarget,
1386
+ underCmux: true,
1387
+ });
1388
+ const command = args[0];
1389
+ // `=`, `%`, and spaces round-trip intact inside single quotes.
1390
+ assert.match(command, /^env /);
1391
+ assert.ok(command.includes(`OMX_SESSION_ID='${trickySessionId}'`));
1392
+ assert.ok(command.includes(`OMX_QUESTION_RETURN_TARGET='${trickyReturnTarget}'`));
1393
+ assert.equal(command.includes(' -e '), false);
1394
+ });
1395
+ it('escapes embedded single quotes in env values under cmux', () => {
1396
+ const args = buildQuestionUiTmuxArgs(recordPath, {
1397
+ cwd: '/repo',
1398
+ sessionId: "a'b=c",
1399
+ underCmux: true,
1400
+ });
1401
+ // POSIX single-quote escaping: a'b=c -> 'a'\''b=c'
1402
+ assert.ok(args[0].includes("OMX_SESSION_ID='a'\\''b=c'"));
1403
+ });
1404
+ it('omits the env prefix entirely when there are no env vars under cmux', () => {
1405
+ const args = buildQuestionUiTmuxArgs(recordPath, { cwd: '/repo', underCmux: true });
1406
+ assert.equal(args.length, 1);
1407
+ assert.equal(args[0].startsWith('env '), false);
1408
+ assert.equal(args[0].includes('export'), false);
1409
+ assert.equal(args[0].includes('&&'), false);
1410
+ assert.ok(args[0].startsWith(`'${process.execPath}' `));
1411
+ });
1412
+ });
1413
+ describe('launchQuestionRenderer under cmux', () => {
1414
+ it('drops bare -e and exports env so the cmux split pane runs the real command', () => {
1415
+ const calls = [];
1416
+ const result = launchQuestionRenderer({
1417
+ cwd: '/repo',
1418
+ recordPath: '/repo/.omx/state/sessions/s1/questions/question-cmux.json',
1419
+ sessionId: 's1',
1420
+ env: {
1421
+ TMUX: '/tmp/tmux-demo',
1422
+ TMUX_PANE: '%11',
1423
+ CMUX_SOCKET_PATH: '/tmp/cmux.sock',
1424
+ },
1425
+ }, {
1426
+ strategy: 'inside-tmux',
1427
+ execTmux: (args) => {
1428
+ calls.push(args);
1429
+ if (args[0] === 'display-message' && args.includes('#{pane_height}'))
1430
+ return '40\n';
1431
+ if (args[0] === 'display-message')
1432
+ return '1\n';
1433
+ if (args[0] === 'split-window')
1434
+ return '%55\n';
1435
+ if (args[0] === 'list-panes')
1436
+ return '0\t%55\n';
1437
+ return '';
1438
+ },
1439
+ sleepSync: () => { },
1440
+ });
1441
+ assert.equal(result.target, '%55');
1442
+ const splitCall = calls.find((call) => call[0] === 'split-window');
1443
+ assert.ok(splitCall);
1444
+ // cwd (-c) and pane flags are preserved exactly as on real tmux.
1445
+ assert.ok(splitCall.includes('-c'));
1446
+ assert.ok(splitCall.includes('/repo'));
1447
+ assert.ok(splitCall.includes('-P'));
1448
+ // No bare `-e` leaks into the cmux pane command (the original bug).
1449
+ assert.equal(splitCall.includes('-e'), false);
1450
+ // The pane command is a single shell-neutral env-prefixed shell-command argument.
1451
+ const paneCommand = splitCall[splitCall.length - 1];
1452
+ assert.match(paneCommand, /^env /);
1453
+ assert.equal(paneCommand.includes(' -e '), false);
1454
+ assert.equal(paneCommand.includes('export'), false);
1455
+ assert.ok(paneCommand.includes("OMX_SESSION_ID='s1'"));
1456
+ assert.ok(paneCommand.includes("OMX_QUESTION_RETURN_TARGET='%11'"));
1457
+ // env runs the real command (quoted node), never `-e`.
1458
+ assert.ok(paneCommand.includes(`'${process.execPath}' `));
1459
+ });
1460
+ });
896
1461
  //# sourceMappingURL=renderer.test.js.map