handmux 0.23.0 → 0.25.0

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 (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -0,0 +1,121 @@
1
+ import { serializePaneInput } from '../paneInput.js';
2
+ import { codex, codexExitSessionId } from './codex.js';
3
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
+ const SHELLS = new Set(['sh', 'bash', 'zsh', 'fish', 'dash', 'ksh', 'tcsh']);
5
+ function throwIfAborted(signal) {
6
+ if (signal.aborted)
7
+ throw signal.reason instanceof Error ? signal.reason : new Error('Activation cancelled');
8
+ }
9
+ export function createCodexConversationActivationController({ panes, process, commands, wait = pause, }) {
10
+ if (!panes || !process || !commands || typeof commands.sendKey !== 'function'
11
+ || typeof commands.capturePlain !== 'function' || typeof commands.runPaneCommand !== 'function') {
12
+ throw new TypeError('Codex Conversation activation requires pane control');
13
+ }
14
+ const pane = async (paneId) => (await panes.list()).find((item) => item.paneId === paneId) ?? null;
15
+ const codexIdentity = async (paneId) => {
16
+ const current = await pane(paneId);
17
+ if (!current)
18
+ return null;
19
+ const identity = await process.inspectForeground(current);
20
+ if (!identity)
21
+ return null;
22
+ return await codex.process.verify(current, { inspectForeground: async () => identity })
23
+ ? identity : null;
24
+ };
25
+ const shellIdentity = async (paneId) => {
26
+ const current = await pane(paneId);
27
+ if (!current)
28
+ return null;
29
+ const identity = await process.inspectForeground(current);
30
+ if (!identity)
31
+ return null;
32
+ const executable = identity.executable?.split('/').pop()?.toLowerCase();
33
+ return SHELLS.has(current.currentCommand.toLowerCase()) || (executable && SHELLS.has(executable))
34
+ ? identity : null;
35
+ };
36
+ const sameProcess = (first, second) => (first.pid === second.pid
37
+ && (first.startedAt === undefined || second.startedAt === first.startedAt)
38
+ && (first.tty === undefined || second.tty === first.tty));
39
+ return {
40
+ apiVersion: 1,
41
+ async describe(run) {
42
+ if (run.ref.sessionId || run.signal.aborted || !await codexIdentity(run.ref.paneId))
43
+ return null;
44
+ return { effect: 'replace-process-preserve-session' };
45
+ },
46
+ async activate(run, signal) {
47
+ await serializePaneInput(run.ref.paneId, async () => {
48
+ const paneId = run.ref.paneId;
49
+ throwIfAborted(signal);
50
+ const original = !run.ref.sessionId && !run.signal.aborted
51
+ ? await codexIdentity(paneId) : null;
52
+ throwIfAborted(signal);
53
+ if (!original)
54
+ throw new Error('The Agent run changed before Conversation activation');
55
+ // Authorization is bound to the verified run above. Exiting that exact process intentionally revokes
56
+ // its lease, so subsequent checks bind recovery to the shell that replaced it.
57
+ let exited = false;
58
+ let shell = null;
59
+ for (let press = 0; press < 2 && !exited; press += 1) {
60
+ throwIfAborted(signal);
61
+ const beforePress = await codexIdentity(paneId);
62
+ if (!beforePress || !sameProcess(original, beforePress)) {
63
+ throw new Error('The Agent process changed before Conversation activation');
64
+ }
65
+ throwIfAborted(signal);
66
+ await commands.sendKey(paneId, 'C-c');
67
+ for (let attempt = 0; attempt < 10; attempt += 1) {
68
+ await wait(500);
69
+ throwIfAborted(signal);
70
+ if (!await pane(paneId))
71
+ throw new Error('The pane closed during Conversation activation');
72
+ const currentIdentity = await codexIdentity(paneId);
73
+ if (!currentIdentity) {
74
+ shell = await shellIdentity(paneId);
75
+ if (!shell)
76
+ throw new Error('The pane did not return to the expected shell');
77
+ exited = true;
78
+ break;
79
+ }
80
+ if (!sameProcess(original, currentIdentity)) {
81
+ throw new Error('The Agent process changed during Conversation activation');
82
+ }
83
+ }
84
+ }
85
+ if (!exited || !shell)
86
+ throw new Error('Codex did not exit; close any open panel in the terminal and try again');
87
+ let sessionId = null;
88
+ for (let attempt = 0; attempt < 20; attempt += 1) {
89
+ throwIfAborted(signal);
90
+ const beforeCapture = await shellIdentity(paneId);
91
+ if (!beforeCapture || !sameProcess(shell, beforeCapture)) {
92
+ throw new Error('The pane shell changed during Conversation activation');
93
+ }
94
+ try {
95
+ const candidate = codexExitSessionId(await commands.capturePlain(paneId));
96
+ if (codex.sessions.isId(candidate)) {
97
+ sessionId = candidate;
98
+ break;
99
+ }
100
+ }
101
+ catch {
102
+ throw new Error('The pane closed during Conversation activation');
103
+ }
104
+ await wait(100);
105
+ }
106
+ if (!sessionId)
107
+ throw new Error('Codex did not expose a resumable session; continue in the terminal');
108
+ const resume = codex.sessions.managedResumeCmd?.(sessionId);
109
+ if (!resume)
110
+ throw new Error('Managed Conversation activation is unavailable');
111
+ throwIfAborted(signal);
112
+ const beforeResume = await shellIdentity(paneId);
113
+ if (!beforeResume || !sameProcess(shell, beforeResume)) {
114
+ throw new Error('The pane shell changed before Conversation activation');
115
+ }
116
+ throwIfAborted(signal);
117
+ await commands.runPaneCommand(paneId, resume);
118
+ });
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,256 @@
1
+ import { stat as statFile } from 'node:fs/promises';
2
+ import { parseCodexGoal } from '../codexStreamProtocol.js';
3
+ import { readLatestContextUsage } from '../codexUsageSnapshot.js';
4
+ import { resolveCodexRollout, sessionsDir } from './codex.js';
5
+ const CONTEXT_RECOVERY_TTL_MS = 2_000;
6
+ const CONTEXT_RECOVERY_CACHE_LIMIT = 64;
7
+ const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null);
8
+ const string = (value) => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
9
+ function target(run) {
10
+ const threadId = run.ref.sessionId;
11
+ if (!threadId)
12
+ throw new Error('Codex conversation control requires a managed thread');
13
+ return { pane: run.ref.paneId, threadId };
14
+ }
15
+ const PERMISSION_SETTINGS = {
16
+ default: {
17
+ approvalPolicy: 'on-request', approvalsReviewer: 'user',
18
+ sandboxPolicy: { type: 'workspaceWrite' },
19
+ },
20
+ 'auto-review': {
21
+ approvalPolicy: 'on-request', approvalsReviewer: 'auto_review',
22
+ sandboxPolicy: { type: 'workspaceWrite' },
23
+ },
24
+ 'full-access': {
25
+ approvalPolicy: 'never', approvalsReviewer: 'user',
26
+ sandboxPolicy: { type: 'dangerFullAccess' },
27
+ },
28
+ };
29
+ function permissionMode(settings) {
30
+ const sandbox = string(record(settings.sandboxPolicy)?.type);
31
+ const approval = string(settings.approvalPolicy);
32
+ const reviewer = string(settings.approvalsReviewer);
33
+ if ((sandbox === 'dangerFullAccess' || sandbox === 'danger-full-access') && approval === 'never') {
34
+ return 'full-access';
35
+ }
36
+ if ((sandbox === 'workspaceWrite' || sandbox === 'workspace-write') && approval === 'on-request') {
37
+ return reviewer === 'auto_review' || reviewer === 'guardian_subagent' ? 'auto-review' : 'default';
38
+ }
39
+ return 'custom';
40
+ }
41
+ function contextSnapshot(value) {
42
+ const status = record(value) ?? {};
43
+ const settings = record(status.settings) ?? {};
44
+ const usage = record(status.contextUsage);
45
+ const nativeStatus = record(status.status);
46
+ const flags = Array.isArray(nativeStatus?.activeFlags) ? nativeStatus.activeFlags : [];
47
+ const activity = status.activityKind === 'compacting' ? 'compacting'
48
+ : flags.includes('waitingOnApproval') || flags.includes('waitingOnUserInput')
49
+ || (Array.isArray(status.approvals) && status.approvals.length > 0)
50
+ || (Array.isArray(status.userInputs) && status.userInputs.length > 0) ? 'waiting'
51
+ : nativeStatus?.type === 'active' ? 'working' : 'idle';
52
+ const sandbox = string(record(settings.sandboxPolicy)?.type);
53
+ const access = sandbox === 'readOnly' || sandbox === 'read-only' ? 'read-only'
54
+ : sandbox === 'workspaceWrite' || sandbox === 'workspace-write' ? 'workspace-write'
55
+ : sandbox === 'dangerFullAccess' || sandbox === 'danger-full-access' ? 'full-access' : undefined;
56
+ const usedTokens = typeof usage?.usedTokens === 'number' ? usage.usedTokens : undefined;
57
+ const totalTokens = typeof usage?.totalTokens === 'number' ? usage.totalTokens : undefined;
58
+ const cwd = string(settings.cwd);
59
+ const branch = string(status.gitBranch);
60
+ return {
61
+ activity,
62
+ ...(usedTokens === undefined ? {} : { usedTokens }),
63
+ ...(totalTokens === undefined ? {} : { totalTokens }),
64
+ ...(cwd === undefined ? {} : { cwd }),
65
+ ...(branch === undefined ? {} : { branch }),
66
+ ...(access === undefined ? {} : { access }),
67
+ };
68
+ }
69
+ function permissionSnapshot(value) {
70
+ const status = record(value) ?? {};
71
+ const settings = record(status.settings) ?? {};
72
+ return {
73
+ mode: permissionMode(settings),
74
+ options: ['default', 'auto-review', 'full-access'],
75
+ };
76
+ }
77
+ function planSnapshot(value) {
78
+ const status = record(value) ?? {};
79
+ const plan = record(status.plan);
80
+ const stepsValue = Array.isArray(plan?.steps) ? plan.steps : Array.isArray(plan?.plan) ? plan.plan : [];
81
+ const steps = stepsValue.flatMap((candidate) => {
82
+ const step = record(candidate);
83
+ const label = string(step?.step);
84
+ const state = ['pending', 'inProgress', 'completed'].find((item) => item === step?.status);
85
+ return label && state ? [{ step: label, status: state }] : [];
86
+ });
87
+ if (!steps.length)
88
+ return null;
89
+ const explanation = string(plan?.explanation);
90
+ const turnId = string(plan?.turnId) ?? string(status.activeTurnId);
91
+ const waiting = contextSnapshot(status).activity === 'waiting';
92
+ return {
93
+ steps,
94
+ ...(explanation === undefined ? {} : { explanation }),
95
+ ...(turnId === undefined ? {} : { turnId }),
96
+ ...(waiting ? { waiting: true } : {}),
97
+ };
98
+ }
99
+ export function createCodexConversationControls(app, clear, { sessionsRoot = sessionsDir(), findRollout = resolveCodexRollout, reader = readLatestContextUsage, stat = statFile, now = Date.now, } = {}) {
100
+ let cached;
101
+ const recoveredContexts = new Map();
102
+ const storeRecovered = (threadId, entry) => {
103
+ if (recoveredContexts.has(threadId))
104
+ recoveredContexts.delete(threadId);
105
+ if (recoveredContexts.size >= CONTEXT_RECOVERY_CACHE_LIMIT) {
106
+ const oldest = recoveredContexts.keys().next().value;
107
+ if (oldest !== undefined)
108
+ recoveredContexts.delete(oldest);
109
+ }
110
+ recoveredContexts.set(threadId, entry);
111
+ };
112
+ const recoverContext = (threadId) => {
113
+ const checkedAt = now();
114
+ const previous = recoveredContexts.get(threadId);
115
+ if (previous?.inFlight)
116
+ return previous.inFlight;
117
+ if (previous && previous.checkedAt + CONTEXT_RECOVERY_TTL_MS > checkedAt) {
118
+ return Promise.resolve(previous.value);
119
+ }
120
+ let pending;
121
+ pending = (async () => {
122
+ try {
123
+ const file = await findRollout(sessionsRoot, threadId);
124
+ if (!file) {
125
+ storeRecovered(threadId, { checkedAt, file: null, value: null });
126
+ return null;
127
+ }
128
+ const metadata = await stat(file);
129
+ if (previous?.file === file && previous.size === metadata.size
130
+ && previous.mtimeMs === metadata.mtimeMs) {
131
+ storeRecovered(threadId, {
132
+ checkedAt,
133
+ file,
134
+ size: metadata.size,
135
+ mtimeMs: metadata.mtimeMs,
136
+ value: previous.value,
137
+ });
138
+ return previous.value;
139
+ }
140
+ const value = await reader(file);
141
+ storeRecovered(threadId, {
142
+ checkedAt, file, size: metadata.size, mtimeMs: metadata.mtimeMs, value,
143
+ });
144
+ return value;
145
+ }
146
+ catch {
147
+ const fallback = previous ?? { checkedAt, file: null, value: null };
148
+ storeRecovered(threadId, {
149
+ checkedAt,
150
+ file: fallback.file,
151
+ ...(fallback.size === undefined ? {} : { size: fallback.size }),
152
+ ...(fallback.mtimeMs === undefined ? {} : { mtimeMs: fallback.mtimeMs }),
153
+ value: fallback.value,
154
+ });
155
+ return fallback.value;
156
+ }
157
+ })();
158
+ storeRecovered(threadId, {
159
+ checkedAt,
160
+ file: previous?.file ?? null,
161
+ ...(previous?.size === undefined ? {} : { size: previous.size }),
162
+ ...(previous?.mtimeMs === undefined ? {} : { mtimeMs: previous.mtimeMs }),
163
+ value: previous?.value ?? null,
164
+ inFlight: pending,
165
+ });
166
+ return pending;
167
+ };
168
+ const status = (pane, threadId) => {
169
+ const key = `${pane}\0${threadId}`;
170
+ if (cached?.key === key && cached.expiresAt >= now())
171
+ return cached.value;
172
+ const value = app.status(pane, threadId);
173
+ cached = { key, expiresAt: now() + 100, value };
174
+ void value.catch(() => { if (cached?.value === value)
175
+ cached = undefined; });
176
+ return value;
177
+ };
178
+ const invalidate = () => { cached = undefined; };
179
+ return {
180
+ goal: {
181
+ apiVersion: 1,
182
+ async read(run) {
183
+ const { pane, threadId } = target(run);
184
+ return parseCodexGoal(await app.getGoal(pane, threadId));
185
+ },
186
+ async start(run, objective) {
187
+ const { pane, threadId } = target(run);
188
+ const goal = parseCodexGoal(await app.startGoal(pane, threadId, objective));
189
+ if (!goal)
190
+ throw new Error('Codex returned an invalid Goal');
191
+ invalidate();
192
+ return goal;
193
+ },
194
+ async update(run, patch) {
195
+ const { pane, threadId } = target(run);
196
+ const goal = parseCodexGoal(await app.updateGoal(pane, threadId, patch));
197
+ if (!goal)
198
+ throw new Error('Codex returned an invalid Goal');
199
+ invalidate();
200
+ return goal;
201
+ },
202
+ async clear(run) {
203
+ const { pane, threadId } = target(run);
204
+ await app.clearGoal(pane, threadId);
205
+ invalidate();
206
+ },
207
+ },
208
+ plan: {
209
+ apiVersion: 1,
210
+ async read(run) {
211
+ const { pane, threadId } = target(run);
212
+ return planSnapshot(await status(pane, threadId));
213
+ },
214
+ },
215
+ context: {
216
+ apiVersion: 1,
217
+ async read(run) {
218
+ const { pane, threadId } = target(run);
219
+ const snapshot = contextSnapshot(await status(pane, threadId));
220
+ if (snapshot.usedTokens !== undefined && snapshot.totalTokens !== undefined)
221
+ return snapshot;
222
+ const recovered = await recoverContext(threadId);
223
+ return recovered ? {
224
+ ...snapshot,
225
+ usedTokens: recovered.usedTokens,
226
+ totalTokens: recovered.totalTokens,
227
+ } : snapshot;
228
+ },
229
+ },
230
+ permission: {
231
+ apiVersion: 1,
232
+ async read(run) {
233
+ const { pane, threadId } = target(run);
234
+ return permissionSnapshot(await status(pane, threadId));
235
+ },
236
+ async update(run, mode) {
237
+ const { pane, threadId } = target(run);
238
+ await app.updateSettings(pane, threadId, PERMISSION_SETTINGS[mode]);
239
+ invalidate();
240
+ return permissionSnapshot(await status(pane, threadId));
241
+ },
242
+ },
243
+ commands: {
244
+ apiVersion: 1,
245
+ commands: ['compact', 'clear'],
246
+ async execute(run, command) {
247
+ const { pane, threadId } = target(run);
248
+ if (command === 'compact')
249
+ await app.compact(pane, threadId);
250
+ else
251
+ await clear(pane, threadId);
252
+ invalidate();
253
+ },
254
+ },
255
+ };
256
+ }
@@ -0,0 +1,306 @@
1
+ function decisionId(value) {
2
+ return typeof value === 'string' ? value : value.id;
3
+ }
4
+ function decisionLabel(value) {
5
+ if (typeof value === 'string') {
6
+ return {
7
+ accept: 'Allow once', acceptForSession: 'Allow for session', decline: 'Deny', cancel: 'Cancel',
8
+ }[value] ?? value;
9
+ }
10
+ if (value.type === 'networkPolicy') {
11
+ return `${value.action === 'deny' ? 'Deny' : 'Allow'} network${value.host ? `: ${value.host}` : ''}`;
12
+ }
13
+ return `${value.action === 'deny' ? 'Deny' : 'Allow'} command policy`;
14
+ }
15
+ function approval(value) {
16
+ const sourceId = `approval:${value.id}`;
17
+ const intent = value.type === 'command' ? 'command_approval'
18
+ : value.type === 'file' ? 'file_approval' : 'permission_approval';
19
+ const prompt = value.reason || value.command || value.type;
20
+ const details = [
21
+ ...(value.reason ? [{ kind: 'reason', type: 'text', text: value.reason }] : []),
22
+ ...(value.command ? [{ kind: 'command', type: 'code', text: value.command }] : []),
23
+ ...(value.cwd ? [{
24
+ kind: 'working_directory', type: 'path', text: value.cwd,
25
+ }] : []),
26
+ ];
27
+ const options = [...new Map(value.decisions.map((decision) => [
28
+ decisionId(decision), { id: decisionId(decision), label: decisionLabel(decision) },
29
+ ])).values()];
30
+ if (!options.length) {
31
+ return {
32
+ sourceId,
33
+ requestId: value.id,
34
+ kind: 'local_only',
35
+ pending: {
36
+ id: sourceId, type: 'local_only', intent, prompt,
37
+ ...(details.length ? { details } : {}),
38
+ ...(value.correlationId || value.itemId || value.turnId
39
+ ? { correlationId: value.correlationId || value.itemId || value.turnId } : {}),
40
+ },
41
+ };
42
+ }
43
+ return {
44
+ sourceId,
45
+ requestId: value.id,
46
+ kind: 'approval',
47
+ decisionIds: new Set(options.map((option) => option.id)),
48
+ pending: {
49
+ id: sourceId, type: 'approval', intent, prompt, options,
50
+ ...(details.length ? { details } : {}),
51
+ ...(value.correlationId || value.itemId || value.turnId
52
+ ? { correlationId: value.correlationId || value.itemId || value.turnId } : {}),
53
+ },
54
+ };
55
+ }
56
+ function userInput(value) {
57
+ const sourceId = `input:${value.id}`;
58
+ const correlationId = value.correlationId || value.itemId || value.turnId;
59
+ if (!value.questions.length) {
60
+ return {
61
+ sourceId, requestId: value.id, kind: 'local_only',
62
+ pending: {
63
+ id: sourceId, type: 'local_only', intent: 'input_request', prompt: value.id,
64
+ ...(correlationId ? { correlationId } : {}),
65
+ },
66
+ };
67
+ }
68
+ const inputFields = new Map();
69
+ const fields = [];
70
+ for (const [questionIndex, question] of value.questions.entries()) {
71
+ const fieldId = `field:${questionIndex}`;
72
+ if (question.options) {
73
+ const usableOptions = question.options.filter((option) => option.label.length > 0);
74
+ if (usableOptions.length !== question.options.length || !usableOptions.length)
75
+ continue;
76
+ const optionValues = new Map();
77
+ const options = usableOptions.map((option, optionIndex) => {
78
+ const id = `option:${questionIndex}:${optionIndex}`;
79
+ optionValues.set(id, option.label);
80
+ return {
81
+ id, label: option.label,
82
+ ...(option.description ? { description: option.description } : {}),
83
+ };
84
+ });
85
+ inputFields.set(fieldId, {
86
+ questionId: question.id, optionValues,
87
+ ...(question.isOther ? { allowOther: true } : {}),
88
+ });
89
+ fields.push({
90
+ id: fieldId, type: 'select', prompt: question.question, options,
91
+ ...(question.header ? { label: question.header } : {}),
92
+ ...(question.isOther ? { allowOther: true } : {}),
93
+ });
94
+ continue;
95
+ }
96
+ inputFields.set(fieldId, { questionId: question.id });
97
+ fields.push({
98
+ id: fieldId, type: question.isSecret ? 'secret' : 'text',
99
+ prompt: question.question,
100
+ ...(question.header ? { label: question.header } : {}),
101
+ });
102
+ }
103
+ if (fields.length !== value.questions.length) {
104
+ return {
105
+ sourceId, requestId: value.id, kind: 'local_only',
106
+ pending: {
107
+ id: sourceId, type: 'local_only', intent: 'input_request',
108
+ prompt: value.questions.map((question) => question.question).join('\n'),
109
+ ...(correlationId ? { correlationId } : {}),
110
+ },
111
+ };
112
+ }
113
+ return {
114
+ sourceId, requestId: value.id, kind: 'input', inputFields,
115
+ pending: {
116
+ id: sourceId, type: 'form', intent: 'input_request',
117
+ prompt: value.questions[0].question, fields,
118
+ ...(correlationId ? { correlationId } : {}),
119
+ },
120
+ };
121
+ }
122
+ function normalizeSnapshot(snapshot) {
123
+ const values = [
124
+ ...snapshot.approvals.map(approval),
125
+ ...snapshot.userInputs.map(userInput),
126
+ ];
127
+ return new Map(values.map((value) => [value.sourceId, value]));
128
+ }
129
+ function sourceCursor(cursor, ordinal) {
130
+ return `codex-interactions:${cursor}:${ordinal}`;
131
+ }
132
+ function errorReceipt(error) {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ if (/no longer pending/i.test(message))
135
+ return { status: 'already_resolved' };
136
+ if (/session changed|not managed|stale/i.test(message))
137
+ return { status: 'stale_run' };
138
+ if (/unavailable|bad user input|unsupported|decision/i.test(message)) {
139
+ return { status: 'rejected', reason: 'provider_rejected' };
140
+ }
141
+ return { status: 'unknown', reason: 'temporarily_unavailable' };
142
+ }
143
+ export function createCodexInteractionAdapter(app) {
144
+ if (!app || typeof app.discover !== 'function' || typeof app.observeInteractions !== 'function'
145
+ || typeof app.decide !== 'function' || typeof app.answerInput !== 'function') {
146
+ throw new TypeError('Codex Interaction adapter requires App Server interaction APIs');
147
+ }
148
+ const bindings = new Map();
149
+ return {
150
+ apiVersion: 1,
151
+ async observeNative(run, sink) {
152
+ const threadId = run.ref.sessionId;
153
+ if (!threadId)
154
+ throw new Error('Codex Interaction run has no thread');
155
+ const discovered = await app.discover(run.ref.paneId);
156
+ if (!discovered?.managed || discovered.threadId !== threadId) {
157
+ throw new Error('Codex Interaction run is not the pane-owned thread');
158
+ }
159
+ let phase = 'opening';
160
+ let current = new Map();
161
+ const buffered = [];
162
+ let tail = Promise.resolve();
163
+ let reset = 0;
164
+ const emitSnapshot = async (snapshot) => {
165
+ if (phase === 'closed' || !Number.isSafeInteger(snapshot.cursor) || snapshot.cursor < 0) {
166
+ throw new Error('Invalid Codex Interaction snapshot');
167
+ }
168
+ const next = normalizeSnapshot(snapshot);
169
+ const events = [];
170
+ for (const [id] of current) {
171
+ if (!next.has(id))
172
+ events.push(snapshot.disconnected
173
+ ? { type: 'cancelled', interactionId: id, reason: 'stream_reset' }
174
+ : { type: 'resolved', interactionId: id });
175
+ }
176
+ for (const [id, item] of next) {
177
+ const previous = current.get(id);
178
+ if (!previous || JSON.stringify(previous.pending) !== JSON.stringify(item.pending)) {
179
+ events.push({ type: 'opened', interaction: item.pending });
180
+ }
181
+ }
182
+ current = next;
183
+ binding.interactions = next;
184
+ for (const [index, event] of events.entries()) {
185
+ const cursor = snapshot.disconnected
186
+ ? `codex-interactions:reset:${++reset}:${index}`
187
+ : sourceCursor(snapshot.cursor, index + 1);
188
+ await sink({ ...event, sourceCursor: cursor });
189
+ }
190
+ };
191
+ const failClosed = async () => {
192
+ if (phase === 'closed')
193
+ return;
194
+ const pending = [...current.keys()];
195
+ current.clear();
196
+ binding.interactions.clear();
197
+ for (const [index, interactionId] of pending.entries()) {
198
+ try {
199
+ await sink({
200
+ type: 'cancelled', interactionId, reason: 'stream_reset',
201
+ sourceCursor: `codex-interactions:reset:${++reset}:${index}`,
202
+ });
203
+ }
204
+ catch {
205
+ break;
206
+ }
207
+ }
208
+ phase = 'closed';
209
+ observed.close();
210
+ };
211
+ const observed = await app.observeInteractions(run.ref.paneId, threadId, (snapshot) => {
212
+ if (phase === 'opening') {
213
+ buffered.push(structuredClone(snapshot));
214
+ return;
215
+ }
216
+ tail = tail.then(async () => {
217
+ await emitSnapshot(snapshot);
218
+ if (snapshot.disconnected) {
219
+ phase = 'closed';
220
+ observed.close();
221
+ }
222
+ }).catch(failClosed);
223
+ });
224
+ current = normalizeSnapshot(observed);
225
+ const previous = bindings.get(run.ref.runId);
226
+ previous?.detachAbort();
227
+ const binding = { run, interactions: current, detachAbort: () => { } };
228
+ const onAbort = () => {
229
+ phase = 'closed';
230
+ observed.close();
231
+ if (bindings.get(run.ref.runId) === binding)
232
+ bindings.delete(run.ref.runId);
233
+ };
234
+ run.signal.addEventListener('abort', onAbort, { once: true });
235
+ binding.detachAbort = () => run.signal.removeEventListener('abort', onAbort);
236
+ bindings.set(run.ref.runId, binding);
237
+ phase = 'live';
238
+ for (const snapshot of buffered.splice(0)) {
239
+ if (snapshot.cursor <= observed.cursor)
240
+ continue;
241
+ tail = tail.then(() => emitSnapshot(snapshot));
242
+ }
243
+ tail = tail.catch(failClosed);
244
+ return {
245
+ checkpoint: {
246
+ sourceCursor: sourceCursor(observed.cursor, 0),
247
+ pending: [...current.values()].map((item) => structuredClone(item.pending)),
248
+ },
249
+ close() {
250
+ if (phase === 'closed')
251
+ return;
252
+ phase = 'closed';
253
+ observed.close();
254
+ },
255
+ };
256
+ },
257
+ async dispatchResponse(run, request) {
258
+ const threadId = run.ref.sessionId;
259
+ const binding = bindings.get(run.ref.runId);
260
+ if (!threadId || !binding || binding.run !== run || run.signal.aborted) {
261
+ return { status: 'stale_run' };
262
+ }
263
+ const native = binding.interactions.get(request.interactionId);
264
+ if (!native)
265
+ return { status: 'already_resolved' };
266
+ if (native.kind === 'local_only')
267
+ return { status: 'rejected', reason: 'local_only' };
268
+ try {
269
+ if (native.kind === 'approval') {
270
+ if (request.value.type !== 'approval' || !native.decisionIds?.has(request.value.optionId)) {
271
+ return { status: 'rejected', reason: 'invalid_value' };
272
+ }
273
+ await app.decide(run.ref.paneId, threadId, native.requestId, request.value.optionId);
274
+ }
275
+ else {
276
+ const answers = inputAnswers(native, request.value);
277
+ if (answers === null)
278
+ return { status: 'rejected', reason: 'invalid_value' };
279
+ await app.answerInput(run.ref.paneId, threadId, native.requestId, answers);
280
+ }
281
+ binding.interactions.delete(request.interactionId);
282
+ return { status: 'accepted' };
283
+ }
284
+ catch (error) {
285
+ return errorReceipt(error);
286
+ }
287
+ },
288
+ };
289
+ }
290
+ function inputAnswers(native, value) {
291
+ if (value.type !== 'form' || native.pending.type !== 'form' || !native.inputFields)
292
+ return null;
293
+ if (Object.keys(value.answers).length !== native.inputFields.size)
294
+ return null;
295
+ const answers = {};
296
+ for (const [fieldId, field] of native.inputFields) {
297
+ const raw = value.answers[fieldId];
298
+ if (typeof raw !== 'string' || !raw.length)
299
+ return null;
300
+ const answer = field.optionValues?.get(raw) ?? (field.allowOther ? raw : field.optionValues ? null : raw);
301
+ if (answer === null)
302
+ return null;
303
+ answers[field.questionId] = [answer];
304
+ }
305
+ return answers;
306
+ }