factory-factory 0.3.4 → 0.3.5

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 (150) hide show
  1. package/dist/client/assets/{_basePickBy-kS1IrKFQ.js → _basePickBy-DOa44aLB.js} +1 -1
  2. package/dist/client/assets/{_baseUniq-DisTPj-I.js → _baseUniq-DuVP6l01.js} +1 -1
  3. package/dist/client/assets/{arc-CWpVTRns.js → arc-BRTVE1a5.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-VXUJARFQ-Dp1AYPMt.js → architectureDiagram-VXUJARFQ-DphOng2K.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-VD42YOAC-DXpmez0y.js → blockDiagram-VD42YOAC-DvVRHTty.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-YG6GDRKO-C6sDxR49.js → c4Diagram-YG6GDRKO-BZaLUCEP.js} +1 -1
  7. package/dist/client/assets/channel-v55vPUhK.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-CQxlRbYu.js → chunk-4BX2VUAB-n69YHsoN.js} +1 -1
  9. package/dist/client/assets/{chunk-55IACEB6-DxubFDep.js → chunk-55IACEB6-C67Ak1Qy.js} +1 -1
  10. package/dist/client/assets/{chunk-B4BG7PRW-BtTQ0im9.js → chunk-B4BG7PRW-_Ja9GXA4.js} +1 -1
  11. package/dist/client/assets/{chunk-DI55MBZ5-C2tNYDZj.js → chunk-DI55MBZ5-C1TAOocX.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-kRQRN6vP.js → chunk-FMBD7UC4-Co8OmD7c.js} +1 -1
  13. package/dist/client/assets/{chunk-QN33PNHL-DP72lCp6.js → chunk-QN33PNHL-ESdvapXF.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-BfxMFZDN.js → chunk-QZHKN3VN-DQTGFT4I.js} +1 -1
  15. package/dist/client/assets/{chunk-TZMSLE5B-G3wPktxK.js → chunk-TZMSLE5B-C7a0BTNr.js} +1 -1
  16. package/dist/client/assets/classDiagram-2ON5EDUG-DV-E9odE.js +1 -0
  17. package/dist/client/assets/classDiagram-v2-WZHVMYZB-DV-E9odE.js +1 -0
  18. package/dist/client/assets/clone-DQgzPMba.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-S5V4N54A-QPsUxyAe.js → cose-bilkent-S5V4N54A-Dg_Elpr2.js} +1 -1
  20. package/dist/client/assets/{dagre-6UL2VRFP-BAmsmcnI.js → dagre-6UL2VRFP-DC_BntL_.js} +1 -1
  21. package/dist/client/assets/{diagram-PSM6KHXK-QA-3O0wr.js → diagram-PSM6KHXK-D4EnEadG.js} +1 -1
  22. package/dist/client/assets/{diagram-QEK2KX5R-VZkRJLXU.js → diagram-QEK2KX5R-DeDgxkkd.js} +1 -1
  23. package/dist/client/assets/{diagram-S2PKOQOG-DwLmogjW.js → diagram-S2PKOQOG-DqCrW2Mz.js} +1 -1
  24. package/dist/client/assets/{erDiagram-Q2GNP2WA-DMKIVzp4.js → erDiagram-Q2GNP2WA-CoIzOJMx.js} +1 -1
  25. package/dist/client/assets/{flowDiagram-NV44I4VS-op3xA_nS.js → flowDiagram-NV44I4VS-H-hzyVxN.js} +1 -1
  26. package/dist/client/assets/{ganttDiagram-JELNMOA3-nrnzVCJK.js → ganttDiagram-JELNMOA3-BZbu1Bvf.js} +1 -1
  27. package/dist/client/assets/{gitGraphDiagram-NY62KEGX-CK2ytvyZ.js → gitGraphDiagram-NY62KEGX-yHNzOwte.js} +1 -1
  28. package/dist/client/assets/{graph-D_lEcPMk.js → graph-0fFVLhuM.js} +1 -1
  29. package/dist/client/assets/index-CzQJ6qnR.css +1 -0
  30. package/dist/client/assets/{index-C2qHRaOH.js → index-D2gG7VAV.js} +249 -251
  31. package/dist/client/assets/{infoDiagram-WHAUD3N6-DO5X8HkW.js → infoDiagram-WHAUD3N6-BdYcSIzA.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-Bnhe4U1x.js → journeyDiagram-XKPGCS4Q-BgD0iboh.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-3W4ZIXB7-DpTaTXko.js → kanban-definition-3W4ZIXB7-2cCK2J0j.js} +1 -1
  34. package/dist/client/assets/{layout-HT5qr--Q.js → layout-UQByNmge.js} +1 -1
  35. package/dist/client/assets/{linear-CDmQ8HG8.js → linear-COsW-oY4.js} +1 -1
  36. package/dist/client/assets/{mindmap-definition-VGOIOE7T-zDr7s4o2.js → mindmap-definition-VGOIOE7T-CnhVGcBr.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-ADFJNKIX-BVBJCvYL.js → pieDiagram-ADFJNKIX-DX0wt7fJ.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-DzdJzkAJ.js → quadrantDiagram-AYHSOK5B-nfJepDyE.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-ChrLzx8S.js → requirementDiagram-UZGBJVZJ-I_tnp42L.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-GIzxIQQB.js → sankeyDiagram-TZEHDZUN-4r_mPLW_.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-WL72ISMW-BHxEpSCU.js → sequenceDiagram-WL72ISMW-DWymCFZa.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-FKZM4ZOC-ByvdYZKl.js → stateDiagram-FKZM4ZOC-CCHlcmzO.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-CuSoER-j.js +1 -0
  44. package/dist/client/assets/{terminal-instance-xdECz0dx.js → terminal-instance-DKydBe-u.js} +1 -1
  45. package/dist/client/assets/{timeline-definition-IT6M3QCI-BcCmIxix.js → timeline-definition-IT6M3QCI-EkYjHzU_.js} +1 -1
  46. package/dist/client/assets/{treemap-KMMF4GRG-CVpLLzhu.js → treemap-KMMF4GRG-BH9wTzrS.js} +1 -1
  47. package/dist/client/assets/{xychartDiagram-PRI3JC2R-DrgKBPUM.js → xychartDiagram-PRI3JC2R-DwOnTNwE.js} +1 -1
  48. package/dist/client/index.html +2 -2
  49. package/dist/prompts/quick-actions/screenshots.md +12 -6
  50. package/dist/prompts/ratchet/dispatch.md +4 -0
  51. package/dist/src/backend/domains/ratchet/bridges.d.ts +5 -1
  52. package/dist/src/backend/domains/ratchet/bridges.d.ts.map +1 -1
  53. package/dist/src/backend/domains/ratchet/ratchet.service.d.ts.map +1 -1
  54. package/dist/src/backend/domains/ratchet/ratchet.service.js +27 -51
  55. package/dist/src/backend/domains/ratchet/ratchet.service.js.map +1 -1
  56. package/dist/src/backend/domains/ratchet/ratchet.service.test.js +18 -18
  57. package/dist/src/backend/domains/ratchet/ratchet.service.test.js.map +1 -1
  58. package/dist/src/backend/domains/session/acp/acp-runtime-manager.d.ts.map +1 -1
  59. package/dist/src/backend/domains/session/acp/acp-runtime-manager.js +5 -1
  60. package/dist/src/backend/domains/session/acp/acp-runtime-manager.js.map +1 -1
  61. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.d.ts +13 -2
  62. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.d.ts.map +1 -1
  63. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.js +808 -106
  64. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.js.map +1 -1
  65. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.js +627 -8
  66. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.js.map +1 -1
  67. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.d.ts +20 -0
  68. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.d.ts.map +1 -1
  69. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.js +16 -0
  70. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.js.map +1 -1
  71. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.js +15 -0
  72. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.js.map +1 -1
  73. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.d.ts +2 -2
  74. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.d.ts.map +1 -1
  75. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.js +32 -1
  76. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.js.map +1 -1
  77. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.test.js +79 -9
  78. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.test.js.map +1 -1
  79. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.d.ts +2 -2
  80. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.d.ts.map +1 -1
  81. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.js +2 -1
  82. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.js.map +1 -1
  83. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.test.js +7 -1
  84. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.test.js.map +1 -1
  85. package/dist/src/backend/domains/session/chat/chat-message-handlers/registry.js +2 -2
  86. package/dist/src/backend/domains/session/chat/chat-message-handlers/registry.js.map +1 -1
  87. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.d.ts +1 -1
  88. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.d.ts.map +1 -1
  89. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.js +19 -10
  90. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.js.map +1 -1
  91. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.test.js +43 -0
  92. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.test.js.map +1 -1
  93. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.d.ts.map +1 -1
  94. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.js +23 -59
  95. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.js.map +1 -1
  96. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.test.js +8 -21
  97. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.test.js.map +1 -1
  98. package/dist/src/backend/domains/session/lifecycle/session.service.d.ts +7 -0
  99. package/dist/src/backend/domains/session/lifecycle/session.service.d.ts.map +1 -1
  100. package/dist/src/backend/domains/session/lifecycle/session.service.js +105 -6
  101. package/dist/src/backend/domains/session/lifecycle/session.service.js.map +1 -1
  102. package/dist/src/backend/domains/session/lifecycle/session.service.test.js +84 -0
  103. package/dist/src/backend/domains/session/lifecycle/session.service.test.js.map +1 -1
  104. package/dist/src/backend/domains/session/session-domain.service.d.ts +6 -0
  105. package/dist/src/backend/domains/session/session-domain.service.d.ts.map +1 -1
  106. package/dist/src/backend/domains/session/session-domain.service.js +26 -1
  107. package/dist/src/backend/domains/session/session-domain.service.js.map +1 -1
  108. package/dist/src/backend/domains/session/session-domain.service.test.js +21 -0
  109. package/dist/src/backend/domains/session/session-domain.service.test.js.map +1 -1
  110. package/dist/src/backend/domains/session/store/session-store.types.d.ts +1 -1
  111. package/dist/src/backend/domains/session/store/session-store.types.d.ts.map +1 -1
  112. package/dist/src/backend/domains/session/store/session-transcript.d.ts +1 -0
  113. package/dist/src/backend/domains/session/store/session-transcript.d.ts.map +1 -1
  114. package/dist/src/backend/domains/session/store/session-transcript.js +8 -0
  115. package/dist/src/backend/domains/session/store/session-transcript.js.map +1 -1
  116. package/dist/src/backend/interceptors/pr-detection.interceptor.d.ts.map +1 -1
  117. package/dist/src/backend/interceptors/pr-detection.interceptor.js +5 -1
  118. package/dist/src/backend/interceptors/pr-detection.interceptor.js.map +1 -1
  119. package/dist/src/backend/interceptors/pr-detection.interceptor.test.js +14 -0
  120. package/dist/src/backend/interceptors/pr-detection.interceptor.test.js.map +1 -1
  121. package/dist/src/backend/orchestration/cli-health.service.d.ts +1 -0
  122. package/dist/src/backend/orchestration/cli-health.service.d.ts.map +1 -1
  123. package/dist/src/backend/orchestration/cli-health.service.js +16 -1
  124. package/dist/src/backend/orchestration/cli-health.service.js.map +1 -1
  125. package/dist/src/backend/orchestration/cli-health.service.test.js +23 -0
  126. package/dist/src/backend/orchestration/cli-health.service.test.js.map +1 -1
  127. package/dist/src/backend/prompts/ratchet-dispatch.d.ts +8 -1
  128. package/dist/src/backend/prompts/ratchet-dispatch.d.ts.map +1 -1
  129. package/dist/src/backend/prompts/ratchet-dispatch.js +20 -3
  130. package/dist/src/backend/prompts/ratchet-dispatch.js.map +1 -1
  131. package/dist/src/backend/services/constants.d.ts +1 -0
  132. package/dist/src/backend/services/constants.d.ts.map +1 -1
  133. package/dist/src/backend/services/constants.js +1 -0
  134. package/dist/src/backend/services/constants.js.map +1 -1
  135. package/dist/src/backend/trpc/index.d.ts +1 -0
  136. package/dist/src/backend/trpc/index.d.ts.map +1 -1
  137. package/dist/src/backend/trpc/session.trpc.d.ts +1 -0
  138. package/dist/src/backend/trpc/session.trpc.d.ts.map +1 -1
  139. package/dist/src/backend/trpc/session.trpc.js +5 -1
  140. package/dist/src/backend/trpc/session.trpc.js.map +1 -1
  141. package/dist/tsconfig.backend.tsbuildinfo +1 -1
  142. package/package.json +1 -1
  143. package/prompts/quick-actions/screenshots.md +12 -6
  144. package/prompts/ratchet/dispatch.md +4 -0
  145. package/dist/client/assets/channel-D5BCeybW.js +0 -1
  146. package/dist/client/assets/classDiagram-2ON5EDUG-BUS1JDwr.js +0 -1
  147. package/dist/client/assets/classDiagram-v2-WZHVMYZB-BUS1JDwr.js +0 -1
  148. package/dist/client/assets/clone-C5xhPZw8.js +0 -1
  149. package/dist/client/assets/index-D5ywoSOH.css +0 -1
  150. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-ynQtAOKH.js +0 -1
@@ -45,7 +45,7 @@ const DEFAULT_COLLABORATION_MODES = [
45
45
  { name: 'Default', mode: 'default' },
46
46
  { name: 'Plan', mode: 'plan' },
47
47
  ];
48
- async function initializeAdapterWithDefaultModel(adapter, codex) {
48
+ async function initializeAdapterWithDefaultModel(adapter, codex, options) {
49
49
  codex.request.mockResolvedValueOnce({});
50
50
  codex.request.mockResolvedValueOnce({
51
51
  requirements: {
@@ -54,7 +54,7 @@ async function initializeAdapterWithDefaultModel(adapter, codex) {
54
54
  },
55
55
  });
56
56
  codex.request.mockResolvedValueOnce({
57
- data: DEFAULT_COLLABORATION_MODES,
57
+ data: options?.collaborationModes ?? DEFAULT_COLLABORATION_MODES,
58
58
  nextCursor: null,
59
59
  });
60
60
  codex.request.mockResolvedValueOnce({
@@ -327,6 +327,11 @@ describe('CodexAppServerAcpAdapter', () => {
327
327
  id: 'item_c1',
328
328
  command: 'pwd',
329
329
  },
330
+ {
331
+ type: 'reasoning',
332
+ id: 'item_r1',
333
+ summary: [{ type: 'summary_text', text: '**Thinking through replay**' }],
334
+ },
330
335
  ],
331
336
  },
332
337
  ],
@@ -340,9 +345,398 @@ describe('CodexAppServerAcpAdapter', () => {
340
345
  const updates = connection.sessionUpdate.mock.calls.map((call) => call[0].update);
341
346
  expect(updates.some((update) => update.sessionUpdate === 'user_message_chunk')).toBe(true);
342
347
  expect(updates.some((update) => update.sessionUpdate === 'agent_message_chunk')).toBe(true);
348
+ expect(updates.some((update) => update.sessionUpdate === 'agent_thought_chunk' &&
349
+ update.content?.type === 'text' &&
350
+ update.content.text === '**Thinking through replay**')).toBe(true);
343
351
  expect(updates.some((update) => update.sessionUpdate === 'tool_call' &&
344
352
  update.status === 'completed' &&
345
- update.toolCallId === 'codex:thread_abc:turn_1:item_c1')).toBe(true);
353
+ update.toolCallId === 'item_c1')).toBe(true);
354
+ });
355
+ it('does not duplicate replayed tool and reasoning updates when matching live events arrive', async () => {
356
+ const { connection } = createMockConnection();
357
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
358
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
359
+ await initializeAdapterWithDefaultModel(adapter, codex);
360
+ codex.request.mockResolvedValueOnce({
361
+ thread: { id: 'thread_replay_dedupe', cwd: '/tmp/workspace' },
362
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
363
+ reasoningEffort: 'medium',
364
+ });
365
+ codex.request.mockResolvedValueOnce({
366
+ thread: {
367
+ id: 'thread_replay_dedupe',
368
+ turns: [
369
+ {
370
+ id: 'turn_replay_dedupe',
371
+ items: [
372
+ {
373
+ type: 'commandExecution',
374
+ id: 'item_replayed_command',
375
+ command: 'cat README.md',
376
+ },
377
+ {
378
+ type: 'reasoning',
379
+ id: 'item_replayed_reasoning',
380
+ summary: [{ type: 'summary_text', text: '**From replay**' }],
381
+ },
382
+ ],
383
+ },
384
+ ],
385
+ },
386
+ });
387
+ await adapter.loadSession({
388
+ sessionId: 'sess_thread_replay_dedupe',
389
+ cwd: '/tmp/workspace',
390
+ mcpServers: [],
391
+ });
392
+ connection.sessionUpdate.mockClear();
393
+ await adapter.handleCodexNotification('item/started', {
394
+ threadId: 'thread_replay_dedupe',
395
+ turnId: 'turn_replay_dedupe',
396
+ item: {
397
+ type: 'commandExecution',
398
+ id: 'item_replayed_command',
399
+ status: 'inProgress',
400
+ command: 'cat README.md',
401
+ },
402
+ });
403
+ await adapter.handleCodexNotification('item/completed', {
404
+ threadId: 'thread_replay_dedupe',
405
+ turnId: 'turn_replay_dedupe',
406
+ item: {
407
+ type: 'commandExecution',
408
+ id: 'item_replayed_command',
409
+ status: 'completed',
410
+ command: 'cat README.md',
411
+ },
412
+ });
413
+ await adapter.handleCodexNotification('item/reasoning/summaryTextDelta', {
414
+ threadId: 'thread_replay_dedupe',
415
+ turnId: 'turn_replay_dedupe',
416
+ itemId: 'item_replayed_reasoning',
417
+ delta: '**From live**',
418
+ });
419
+ await adapter.handleCodexNotification('item/completed', {
420
+ threadId: 'thread_replay_dedupe',
421
+ turnId: 'turn_replay_dedupe',
422
+ item: {
423
+ type: 'reasoning',
424
+ id: 'item_replayed_reasoning',
425
+ status: 'completed',
426
+ summary: [{ type: 'summary_text', text: '**From live**' }],
427
+ },
428
+ });
429
+ expect(connection.sessionUpdate).not.toHaveBeenCalled();
430
+ });
431
+ it('prefers codex callId for tool call ids and enriches command metadata', async () => {
432
+ const { connection } = createMockConnection();
433
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
434
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
435
+ await initializeAdapterWithDefaultModel(adapter, codex);
436
+ codex.request.mockResolvedValueOnce({
437
+ thread: { id: 'thread_call_id', cwd: '/tmp/workspace' },
438
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
439
+ reasoningEffort: 'medium',
440
+ });
441
+ await adapter.newSession({
442
+ cwd: '/tmp/workspace',
443
+ mcpServers: [],
444
+ });
445
+ await adapter.handleCodexNotification('item/started', {
446
+ threadId: 'thread_call_id',
447
+ turnId: 'turn_call_id',
448
+ item: {
449
+ type: 'commandExecution',
450
+ id: 'item_call_id',
451
+ callId: 'call_123',
452
+ status: 'inProgress',
453
+ command: 'cat README.md',
454
+ },
455
+ });
456
+ await adapter.handleCodexNotification('item/completed', {
457
+ threadId: 'thread_call_id',
458
+ turnId: 'turn_call_id',
459
+ item: {
460
+ type: 'commandExecution',
461
+ id: 'item_call_id',
462
+ callId: 'call_123',
463
+ status: 'completed',
464
+ command: 'cat README.md',
465
+ },
466
+ });
467
+ expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
468
+ [
469
+ expect.objectContaining({
470
+ update: expect.objectContaining({
471
+ sessionUpdate: 'tool_call',
472
+ toolCallId: 'call_123',
473
+ title: 'Read README.md',
474
+ kind: 'read',
475
+ locations: [{ path: '/tmp/workspace/README.md' }],
476
+ }),
477
+ }),
478
+ ],
479
+ [
480
+ expect.objectContaining({
481
+ update: expect.objectContaining({
482
+ sessionUpdate: 'tool_call_update',
483
+ toolCallId: 'call_123',
484
+ title: 'Read README.md',
485
+ kind: 'read',
486
+ }),
487
+ }),
488
+ ],
489
+ ]));
490
+ });
491
+ it('parses escaped quotes in quoted command arguments for metadata', async () => {
492
+ const { connection } = createMockConnection();
493
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
494
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
495
+ await initializeAdapterWithDefaultModel(adapter, codex);
496
+ codex.request.mockResolvedValueOnce({
497
+ thread: { id: 'thread_escaped_quotes', cwd: '/tmp/workspace' },
498
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
499
+ reasoningEffort: 'medium',
500
+ });
501
+ await adapter.newSession({
502
+ cwd: '/tmp/workspace',
503
+ mcpServers: [],
504
+ });
505
+ await adapter.handleCodexNotification('item/started', {
506
+ threadId: 'thread_escaped_quotes',
507
+ turnId: 'turn_escaped_quotes',
508
+ item: {
509
+ type: 'commandExecution',
510
+ id: 'item_escaped_quotes',
511
+ status: 'inProgress',
512
+ command: 'cat "nested \\"quotes\\".md"',
513
+ },
514
+ });
515
+ await adapter.handleCodexNotification('item/completed', {
516
+ threadId: 'thread_escaped_quotes',
517
+ turnId: 'turn_escaped_quotes',
518
+ item: {
519
+ type: 'commandExecution',
520
+ id: 'item_escaped_quotes',
521
+ status: 'completed',
522
+ command: 'cat "nested \\"quotes\\".md"',
523
+ },
524
+ });
525
+ expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
526
+ [
527
+ expect.objectContaining({
528
+ update: expect.objectContaining({
529
+ sessionUpdate: 'tool_call',
530
+ title: 'Read nested "quotes".md',
531
+ kind: 'read',
532
+ locations: [{ path: '/tmp/workspace/nested "quotes".md' }],
533
+ }),
534
+ }),
535
+ ],
536
+ [
537
+ expect.objectContaining({
538
+ update: expect.objectContaining({
539
+ sessionUpdate: 'tool_call_update',
540
+ title: 'Read nested "quotes".md',
541
+ kind: 'read',
542
+ locations: [{ path: '/tmp/workspace/nested "quotes".md' }],
543
+ }),
544
+ }),
545
+ ],
546
+ ]));
547
+ });
548
+ it('parses escaped single quotes and escaped spaces in command metadata', async () => {
549
+ const { connection } = createMockConnection();
550
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
551
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
552
+ await initializeAdapterWithDefaultModel(adapter, codex);
553
+ codex.request.mockResolvedValueOnce({
554
+ thread: { id: 'thread_escaped_single_quotes', cwd: '/tmp/workspace' },
555
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
556
+ reasoningEffort: 'medium',
557
+ });
558
+ await adapter.newSession({
559
+ cwd: '/tmp/workspace',
560
+ mcpServers: [],
561
+ });
562
+ await adapter.handleCodexNotification('item/started', {
563
+ threadId: 'thread_escaped_single_quotes',
564
+ turnId: 'turn_escaped_single_quotes',
565
+ item: {
566
+ type: 'commandExecution',
567
+ id: 'item_escaped_single_quotes',
568
+ status: 'inProgress',
569
+ command: "cat 'it\\'s file.txt'",
570
+ },
571
+ });
572
+ await adapter.handleCodexNotification('item/completed', {
573
+ threadId: 'thread_escaped_single_quotes',
574
+ turnId: 'turn_escaped_single_quotes',
575
+ item: {
576
+ type: 'commandExecution',
577
+ id: 'item_escaped_single_quotes',
578
+ status: 'completed',
579
+ command: "cat 'it\\'s file.txt'",
580
+ },
581
+ });
582
+ expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
583
+ [
584
+ expect.objectContaining({
585
+ update: expect.objectContaining({
586
+ sessionUpdate: 'tool_call',
587
+ title: "Read it's file.txt",
588
+ kind: 'read',
589
+ locations: [{ path: "/tmp/workspace/it's file.txt" }],
590
+ }),
591
+ }),
592
+ ],
593
+ [
594
+ expect.objectContaining({
595
+ update: expect.objectContaining({
596
+ sessionUpdate: 'tool_call_update',
597
+ title: "Read it's file.txt",
598
+ kind: 'read',
599
+ locations: [{ path: "/tmp/workspace/it's file.txt" }],
600
+ }),
601
+ }),
602
+ ],
603
+ ]));
604
+ });
605
+ it('derives command metadata from actionable chained subcommands', async () => {
606
+ const { connection } = createMockConnection();
607
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
608
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
609
+ await initializeAdapterWithDefaultModel(adapter, codex);
610
+ codex.request.mockResolvedValueOnce({
611
+ thread: { id: 'thread_chained_command', cwd: '/tmp/workspace' },
612
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
613
+ reasoningEffort: 'medium',
614
+ });
615
+ await adapter.newSession({
616
+ cwd: '/tmp/workspace',
617
+ mcpServers: [],
618
+ });
619
+ await adapter.handleCodexNotification('item/started', {
620
+ threadId: 'thread_chained_command',
621
+ turnId: 'turn_chained_command',
622
+ item: {
623
+ type: 'commandExecution',
624
+ id: 'item_chained_command',
625
+ status: 'inProgress',
626
+ command: 'cd src && rg "TODO" README.md',
627
+ },
628
+ });
629
+ expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
630
+ [
631
+ expect.objectContaining({
632
+ update: expect.objectContaining({
633
+ sessionUpdate: 'tool_call',
634
+ title: 'Search TODO in README.md',
635
+ kind: 'search',
636
+ locations: [{ path: '/tmp/workspace/README.md' }],
637
+ }),
638
+ }),
639
+ ],
640
+ ]));
641
+ });
642
+ it('keeps escaped apostrophes inside single quotes when splitting chained commands', async () => {
643
+ const { connection } = createMockConnection();
644
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
645
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
646
+ await initializeAdapterWithDefaultModel(adapter, codex);
647
+ codex.request.mockResolvedValueOnce({
648
+ thread: { id: 'thread_chain_single_quote_escape', cwd: '/tmp/workspace' },
649
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
650
+ reasoningEffort: 'medium',
651
+ });
652
+ await adapter.newSession({
653
+ cwd: '/tmp/workspace',
654
+ mcpServers: [],
655
+ });
656
+ await adapter.handleCodexNotification('item/started', {
657
+ threadId: 'thread_chain_single_quote_escape',
658
+ turnId: 'turn_chain_single_quote_escape',
659
+ item: {
660
+ type: 'commandExecution',
661
+ id: 'item_chain_single_quote_escape',
662
+ status: 'inProgress',
663
+ command: "cat 'it\\'s file.txt' && rg TODO README.md",
664
+ },
665
+ });
666
+ expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
667
+ [
668
+ expect.objectContaining({
669
+ update: expect.objectContaining({
670
+ sessionUpdate: 'tool_call',
671
+ title: "Read it's file.txt, Search TODO in README.md",
672
+ kind: 'read',
673
+ locations: [
674
+ { path: "/tmp/workspace/it's file.txt" },
675
+ { path: '/tmp/workspace/README.md' },
676
+ ],
677
+ }),
678
+ }),
679
+ ],
680
+ ]));
681
+ });
682
+ it('avoids duplicate thought chunks when reasoning started item already contains summary text', async () => {
683
+ const { connection } = createMockConnection();
684
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
685
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
686
+ await initializeAdapterWithDefaultModel(adapter, codex);
687
+ codex.request.mockResolvedValueOnce({
688
+ thread: { id: 'thread_reasoning', cwd: '/tmp/workspace' },
689
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
690
+ reasoningEffort: 'medium',
691
+ });
692
+ await adapter.newSession({
693
+ cwd: '/tmp/workspace',
694
+ mcpServers: [],
695
+ });
696
+ await adapter.handleCodexNotification('item/started', {
697
+ threadId: 'thread_reasoning',
698
+ turnId: 'turn_reasoning',
699
+ item: {
700
+ type: 'reasoning',
701
+ id: 'item_reasoning',
702
+ status: 'inProgress',
703
+ summary: [{ type: 'summary_text', text: '**Analyzing approach**' }],
704
+ },
705
+ });
706
+ await adapter.handleCodexNotification('item/reasoning/summaryTextDelta', {
707
+ threadId: 'thread_reasoning',
708
+ turnId: 'turn_reasoning',
709
+ itemId: 'item_reasoning',
710
+ delta: '**Analyzing approach**',
711
+ });
712
+ await adapter.handleCodexNotification('item/completed', {
713
+ threadId: 'thread_reasoning',
714
+ turnId: 'turn_reasoning',
715
+ item: {
716
+ type: 'reasoning',
717
+ id: 'item_reasoning',
718
+ status: 'completed',
719
+ summary: [{ type: 'summary_text', text: '**Analyzing approach**' }],
720
+ },
721
+ });
722
+ const updates = connection.sessionUpdate.mock.calls.map((call) => call[0].update);
723
+ const thoughtChunks = updates.filter((update) => update.sessionUpdate === 'agent_thought_chunk');
724
+ expect(thoughtChunks).toEqual([
725
+ {
726
+ sessionUpdate: 'agent_thought_chunk',
727
+ content: { type: 'text', text: '**Analyzing approach**' },
728
+ },
729
+ ]);
730
+ expect(updates.some((update) => update.sessionUpdate === 'tool_call' &&
731
+ update.toolCallId === 'item_reasoning' &&
732
+ update.kind === 'think' &&
733
+ update.status === 'pending')).toBe(true);
734
+ expect(updates.some((update) => update.sessionUpdate === 'tool_call_update' &&
735
+ update.toolCallId === 'item_reasoning' &&
736
+ update.status === 'in_progress')).toBe(true);
737
+ expect(updates.some((update) => update.sessionUpdate === 'tool_call_update' &&
738
+ update.toolCallId === 'item_reasoning' &&
739
+ update.status === 'completed')).toBe(true);
346
740
  });
347
741
  it('writes provided MCP servers to Codex config and reloads on loadSession', async () => {
348
742
  const { connection } = createMockConnection();
@@ -567,7 +961,7 @@ describe('CodexAppServerAcpAdapter', () => {
567
961
  it('requests ExitPlanMode approval after completed plan item in plan mode', async () => {
568
962
  const { connection } = createMockConnection();
569
963
  connection.requestPermission.mockResolvedValue({
570
- outcome: { outcome: 'selected', optionId: 'allow_once' },
964
+ outcome: { outcome: 'selected', optionId: 'default' },
571
965
  });
572
966
  const { client: codexClient, mocks: codex } = createMockCodexClient();
573
967
  const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
@@ -612,8 +1006,8 @@ describe('CodexAppServerAcpAdapter', () => {
612
1006
  kind: 'switch_mode',
613
1007
  }),
614
1008
  options: [
615
- { optionId: 'allow_once', name: 'Approve plan', kind: 'allow_once' },
616
- { optionId: 'reject_once', name: 'Request changes', kind: 'reject_once' },
1009
+ { optionId: 'default', name: 'Approve and switch to Default', kind: 'allow_once' },
1010
+ { optionId: 'plan', name: 'Keep planning', kind: 'reject_once' },
617
1011
  ],
618
1012
  }));
619
1013
  expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
@@ -659,6 +1053,77 @@ describe('CodexAppServerAcpAdapter', () => {
659
1053
  ],
660
1054
  ]));
661
1055
  });
1056
+ it('switches to the selected non-plan mode id when plan approval is accepted', async () => {
1057
+ const { connection } = createMockConnection();
1058
+ connection.requestPermission.mockResolvedValue({
1059
+ outcome: { outcome: 'selected', optionId: 'code' },
1060
+ });
1061
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
1062
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
1063
+ await initializeAdapterWithDefaultModel(adapter, codex, {
1064
+ collaborationModes: [
1065
+ { name: 'Default', mode: 'default' },
1066
+ { name: 'Code', mode: 'code' },
1067
+ { name: 'Plan', mode: 'plan' },
1068
+ ],
1069
+ });
1070
+ codex.request.mockResolvedValueOnce({
1071
+ thread: { id: 'thread_plan_mode_switch', cwd: '/tmp/workspace' },
1072
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
1073
+ reasoningEffort: 'medium',
1074
+ });
1075
+ const session = await adapter.newSession({
1076
+ cwd: '/tmp/workspace',
1077
+ mcpServers: [],
1078
+ });
1079
+ await adapter.setSessionMode({ sessionId: session.sessionId, modeId: 'plan' });
1080
+ await adapter.handleCodexNotification('item/started', {
1081
+ threadId: 'thread_plan_mode_switch',
1082
+ turnId: 'turn_plan_mode_switch',
1083
+ item: {
1084
+ type: 'plan',
1085
+ id: 'item_plan_mode_switch',
1086
+ status: 'inProgress',
1087
+ },
1088
+ });
1089
+ await adapter.handleCodexNotification('item/plan/delta', {
1090
+ threadId: 'thread_plan_mode_switch',
1091
+ turnId: 'turn_plan_mode_switch',
1092
+ itemId: 'item_plan_mode_switch',
1093
+ delta: '# Plan\n- implement this',
1094
+ });
1095
+ await adapter.handleCodexNotification('item/completed', {
1096
+ threadId: 'thread_plan_mode_switch',
1097
+ turnId: 'turn_plan_mode_switch',
1098
+ item: {
1099
+ type: 'plan',
1100
+ id: 'item_plan_mode_switch',
1101
+ status: 'completed',
1102
+ },
1103
+ });
1104
+ expect(connection.requestPermission).toHaveBeenCalledWith(expect.objectContaining({
1105
+ options: expect.arrayContaining([
1106
+ expect.objectContaining({ optionId: 'default', kind: 'allow_once' }),
1107
+ expect.objectContaining({ optionId: 'code', kind: 'allow_once' }),
1108
+ expect.objectContaining({ optionId: 'plan', kind: 'reject_once' }),
1109
+ ]),
1110
+ }));
1111
+ expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
1112
+ [
1113
+ expect.objectContaining({
1114
+ update: expect.objectContaining({
1115
+ sessionUpdate: 'config_option_update',
1116
+ configOptions: expect.arrayContaining([
1117
+ expect.objectContaining({
1118
+ id: 'mode',
1119
+ currentValue: 'code',
1120
+ }),
1121
+ ]),
1122
+ }),
1123
+ }),
1124
+ ],
1125
+ ]));
1126
+ });
662
1127
  it('holds prompt completion until plan approval resolves and exits plan mode', async () => {
663
1128
  let resolvePlanApproval = () => {
664
1129
  throw new Error('expected plan approval request callback');
@@ -727,7 +1192,7 @@ describe('CodexAppServerAcpAdapter', () => {
727
1192
  await new Promise((resolve) => setTimeout(resolve, 0));
728
1193
  expect(promptSettled).toBe(false);
729
1194
  resolvePlanApproval({
730
- outcome: { outcome: 'selected', optionId: 'allow_once' },
1195
+ outcome: { outcome: 'selected', optionId: 'default' },
731
1196
  });
732
1197
  await itemCompletedPromise;
733
1198
  await expect(promptPromise).resolves.toEqual({ stopReason: 'end_turn' });
@@ -751,7 +1216,7 @@ describe('CodexAppServerAcpAdapter', () => {
751
1216
  it('marks synthetic ExitPlanMode approval as failed when user rejects', async () => {
752
1217
  const { connection } = createMockConnection();
753
1218
  connection.requestPermission.mockResolvedValue({
754
- outcome: { outcome: 'selected', optionId: 'reject_once' },
1219
+ outcome: { outcome: 'selected', optionId: 'plan' },
755
1220
  });
756
1221
  const { client: codexClient, mocks: codex } = createMockCodexClient();
757
1222
  const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
@@ -1287,6 +1752,14 @@ describe('CodexAppServerAcpAdapter', () => {
1287
1752
  },
1288
1753
  });
1289
1754
  expect(codex.respondSuccess).toHaveBeenCalledWith(18, { decision: 'decline' });
1755
+ const sessionUpdateMock = connection.sessionUpdate;
1756
+ const pendingPermissionUpdateIndex = sessionUpdateMock.mock.calls.findIndex((call) => call[0].update?.sessionUpdate === 'tool_call_update' && call[0].update?.status === 'pending');
1757
+ expect(pendingPermissionUpdateIndex).toBeGreaterThanOrEqual(0);
1758
+ const pendingPermissionInvocationOrder = sessionUpdateMock.mock.invocationCallOrder[pendingPermissionUpdateIndex];
1759
+ const requestPermissionInvocationOrder = connection.requestPermission.mock.invocationCallOrder[0];
1760
+ expect(pendingPermissionInvocationOrder).toBeDefined();
1761
+ expect(requestPermissionInvocationOrder).toBeDefined();
1762
+ expect(pendingPermissionInvocationOrder < requestPermissionInvocationOrder).toBe(true);
1290
1763
  expect(connection.sessionUpdate.mock.calls).toEqual(expect.arrayContaining([
1291
1764
  [
1292
1765
  expect.objectContaining({
@@ -1298,6 +1771,152 @@ describe('CodexAppServerAcpAdapter', () => {
1298
1771
  ],
1299
1772
  ]));
1300
1773
  });
1774
+ it('scopes allow_always command approvals by command pattern and cwd', async () => {
1775
+ const { connection } = createMockConnection();
1776
+ connection.requestPermission.mockResolvedValueOnce({
1777
+ outcome: { outcome: 'selected', optionId: 'allow_always' },
1778
+ });
1779
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
1780
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
1781
+ await initializeAdapterWithDefaultModel(adapter, codex);
1782
+ codex.request.mockResolvedValueOnce({
1783
+ thread: { id: 'thread_allow_always', cwd: '/tmp/workspace' },
1784
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
1785
+ reasoningEffort: 'medium',
1786
+ });
1787
+ await adapter.newSession({
1788
+ cwd: '/tmp/workspace',
1789
+ mcpServers: [],
1790
+ });
1791
+ await adapter.handleCodexServerRequest({
1792
+ id: 91,
1793
+ method: 'item/commandExecution/requestApproval',
1794
+ params: {
1795
+ threadId: 'thread_allow_always',
1796
+ turnId: 'turn_allow_always',
1797
+ itemId: 'item_allow_always',
1798
+ command: 'cat README.md',
1799
+ cwd: '/tmp/workspace',
1800
+ },
1801
+ });
1802
+ expect(connection.requestPermission).toHaveBeenCalledWith(expect.objectContaining({
1803
+ toolCall: expect.objectContaining({
1804
+ title: 'Read README.md',
1805
+ kind: 'read',
1806
+ }),
1807
+ options: expect.arrayContaining([
1808
+ expect.objectContaining({ optionId: 'allow_always', kind: 'allow_always' }),
1809
+ expect.objectContaining({ optionId: 'allow_once', kind: 'allow_once' }),
1810
+ expect.objectContaining({ optionId: 'reject_once', kind: 'reject_once' }),
1811
+ ]),
1812
+ }));
1813
+ expect(codex.respondSuccess).toHaveBeenCalledWith(91, { decision: 'accept' });
1814
+ connection.requestPermission.mockClear();
1815
+ codex.respondSuccess.mockClear();
1816
+ await adapter.handleCodexServerRequest({
1817
+ id: 92,
1818
+ method: 'item/commandExecution/requestApproval',
1819
+ params: {
1820
+ threadId: 'thread_allow_always',
1821
+ turnId: 'turn_allow_always',
1822
+ itemId: 'item_allow_always_2',
1823
+ command: 'cat README.md',
1824
+ cwd: '/tmp/workspace',
1825
+ },
1826
+ });
1827
+ expect(connection.requestPermission).not.toHaveBeenCalled();
1828
+ expect(codex.respondSuccess).toHaveBeenCalledWith(92, { decision: 'accept' });
1829
+ codex.respondSuccess.mockClear();
1830
+ await adapter.handleCodexServerRequest({
1831
+ id: 93,
1832
+ method: 'item/commandExecution/requestApproval',
1833
+ params: {
1834
+ threadId: 'thread_allow_always',
1835
+ turnId: 'turn_allow_always',
1836
+ itemId: 'item_allow_always_3',
1837
+ command: 'ls src',
1838
+ cwd: '/tmp/workspace',
1839
+ },
1840
+ });
1841
+ expect(connection.requestPermission).toHaveBeenCalledTimes(1);
1842
+ expect(codex.respondSuccess).toHaveBeenCalledWith(93, { decision: 'accept' });
1843
+ connection.requestPermission.mockClear();
1844
+ codex.respondSuccess.mockClear();
1845
+ await adapter.handleCodexServerRequest({
1846
+ id: 94,
1847
+ method: 'item/commandExecution/requestApproval',
1848
+ params: {
1849
+ threadId: 'thread_allow_always',
1850
+ turnId: 'turn_allow_always',
1851
+ itemId: 'item_allow_always_4',
1852
+ command: 'cat README.md',
1853
+ cwd: '/tmp/other-workspace',
1854
+ },
1855
+ });
1856
+ expect(connection.requestPermission).toHaveBeenCalledTimes(1);
1857
+ expect(codex.respondSuccess).toHaveBeenCalledWith(94, { decision: 'accept' });
1858
+ });
1859
+ it('does not reuse allow_always scope when a chained command changes cwd', async () => {
1860
+ const { connection } = createMockConnection();
1861
+ connection.requestPermission.mockResolvedValueOnce({
1862
+ outcome: { outcome: 'selected', optionId: 'allow_always' },
1863
+ });
1864
+ const { client: codexClient, mocks: codex } = createMockCodexClient();
1865
+ const adapter = new CodexAppServerAcpAdapter(connection, codexClient);
1866
+ await initializeAdapterWithDefaultModel(adapter, codex);
1867
+ codex.request.mockResolvedValueOnce({
1868
+ thread: { id: 'thread_allow_always_cd_scope', cwd: '/tmp/workspace' },
1869
+ approvalPolicy: DEFAULT_APPROVAL_POLICY,
1870
+ reasoningEffort: 'medium',
1871
+ });
1872
+ await adapter.newSession({
1873
+ cwd: '/tmp/workspace',
1874
+ mcpServers: [],
1875
+ });
1876
+ await adapter.handleCodexServerRequest({
1877
+ id: 101,
1878
+ method: 'item/commandExecution/requestApproval',
1879
+ params: {
1880
+ threadId: 'thread_allow_always_cd_scope',
1881
+ turnId: 'turn_allow_always_cd_scope',
1882
+ itemId: 'item_allow_always_cd_scope_1',
1883
+ command: 'cd nested && cat README.md',
1884
+ cwd: '/tmp/workspace',
1885
+ },
1886
+ });
1887
+ expect(connection.requestPermission).toHaveBeenCalledTimes(1);
1888
+ expect(codex.respondSuccess).toHaveBeenCalledWith(101, { decision: 'accept' });
1889
+ connection.requestPermission.mockClear();
1890
+ codex.respondSuccess.mockClear();
1891
+ await adapter.handleCodexServerRequest({
1892
+ id: 102,
1893
+ method: 'item/commandExecution/requestApproval',
1894
+ params: {
1895
+ threadId: 'thread_allow_always_cd_scope',
1896
+ turnId: 'turn_allow_always_cd_scope',
1897
+ itemId: 'item_allow_always_cd_scope_2',
1898
+ command: 'cat README.md',
1899
+ cwd: '/tmp/workspace',
1900
+ },
1901
+ });
1902
+ expect(connection.requestPermission).toHaveBeenCalledTimes(1);
1903
+ expect(codex.respondSuccess).toHaveBeenCalledWith(102, { decision: 'accept' });
1904
+ connection.requestPermission.mockClear();
1905
+ codex.respondSuccess.mockClear();
1906
+ await adapter.handleCodexServerRequest({
1907
+ id: 103,
1908
+ method: 'item/commandExecution/requestApproval',
1909
+ params: {
1910
+ threadId: 'thread_allow_always_cd_scope',
1911
+ turnId: 'turn_allow_always_cd_scope',
1912
+ itemId: 'item_allow_always_cd_scope_3',
1913
+ command: 'cd nested && cat README.md',
1914
+ cwd: '/tmp/workspace',
1915
+ },
1916
+ });
1917
+ expect(connection.requestPermission).not.toHaveBeenCalled();
1918
+ expect(codex.respondSuccess).toHaveBeenCalledWith(103, { decision: 'accept' });
1919
+ });
1301
1920
  it('reconciles turn/completed notifications that arrive before prompt attaches active turn', async () => {
1302
1921
  const { connection } = createMockConnection();
1303
1922
  const { client: codexClient, mocks: codex } = createMockCodexClient();