farming-code 2.2.29 → 2.2.32

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 (385) hide show
  1. package/README.md +25 -66
  2. package/README.zh_cn.md +15 -66
  3. package/THIRD_PARTY_NOTICES.md +7 -0
  4. package/backend/acp/{client-services.js → client-services.cjs} +132 -134
  5. package/backend/acp/packaged-claude-acp.cjs +37 -0
  6. package/backend/acp/packaged-codex-acp.cjs +37 -0
  7. package/backend/acp/patch-decisions.cjs +182 -0
  8. package/backend/acp/permission-security.cjs +131 -0
  9. package/backend/{acp-checkpoint-store.js → acp-checkpoint-store.cjs} +90 -59
  10. package/backend/{acp-runtime.js → acp-runtime.cjs} +1219 -1125
  11. package/backend/acp-session-state.cjs +622 -0
  12. package/backend/acp-transcript.cjs +525 -0
  13. package/backend/agent-env.cjs +257 -0
  14. package/backend/agent-extension-discovery.cjs +452 -0
  15. package/backend/agent-extension-inventory.cjs +116 -0
  16. package/backend/agent-json-stream.cjs +283 -0
  17. package/backend/agent-lifecycle-journal.cjs +185 -0
  18. package/backend/{agent-manager.js → agent-manager.cjs} +3116 -4275
  19. package/backend/agent-order-transaction.cjs +85 -0
  20. package/backend/agent-order.cjs +159 -0
  21. package/backend/agent-provider-session.cjs +98 -0
  22. package/backend/agent-runtime-binding.cjs +179 -0
  23. package/backend/agent-session-history.cjs +1022 -0
  24. package/backend/agent-session-inventory.cjs +291 -0
  25. package/backend/async-cache.cjs +104 -0
  26. package/backend/atomic-json-store.cjs +67 -0
  27. package/backend/auth.cjs +261 -0
  28. package/backend/authoritative-inventory-cache.cjs +383 -0
  29. package/backend/business-health.cjs +68 -0
  30. package/backend/chat-runtime.cjs +50 -0
  31. package/backend/claude-settings.cjs +132 -0
  32. package/backend/cli-agents.cjs +435 -0
  33. package/backend/codex-context-window.cjs +196 -0
  34. package/backend/codex-models.cjs +228 -0
  35. package/backend/codex-session-archive.cjs +82 -0
  36. package/backend/codex-session-history.cjs +590 -0
  37. package/backend/{codex-terminal-profile.js → codex-terminal-profile.cjs} +149 -155
  38. package/backend/codex-transcript-sanitizer.cjs +130 -0
  39. package/backend/codex-transcript.cjs +2107 -0
  40. package/backend/command-runner-child.cjs +105 -0
  41. package/backend/config-manager.cjs +1263 -0
  42. package/backend/control-api.cjs +439 -0
  43. package/backend/data/LICENSE.cua +21 -0
  44. package/backend/executable-discovery.cjs +204 -0
  45. package/backend/farming-agent-bootstrap.cjs +81 -0
  46. package/backend/farming-agent-bootstrap.zh_cn.md +6 -2
  47. package/backend/{farming-app-cli.js → farming-app-cli.cjs} +441 -493
  48. package/backend/farming-cli.cjs +414 -0
  49. package/backend/farming-net-pass.cjs +319 -0
  50. package/backend/farming-net-registry.cjs +123 -0
  51. package/backend/farming-net-server.cjs +309 -0
  52. package/backend/farming-session-store.cjs +651 -0
  53. package/backend/git-worktree-info.cjs +181 -0
  54. package/backend/haiku-token.cjs +828 -0
  55. package/backend/index-html.cjs +96 -0
  56. package/backend/input-parts.cjs +68 -0
  57. package/backend/input-routing.cjs +40 -0
  58. package/backend/{local-session-engine.js → local-session-engine.cjs} +276 -282
  59. package/backend/main-agent-skills.cjs +309 -0
  60. package/backend/main-page-session.cjs +132 -0
  61. package/backend/native-pty-controller-generation.cjs +139 -0
  62. package/backend/{native-pty-host-client.js → native-pty-host-client.cjs} +338 -346
  63. package/backend/native-pty-host-identity.cjs +125 -0
  64. package/backend/native-pty-host-path.cjs +68 -0
  65. package/backend/{native-pty-host.js → native-pty-host.cjs} +480 -569
  66. package/backend/native-session-engine.cjs +296 -0
  67. package/backend/network.cjs +57 -0
  68. package/backend/npm-update-helper.cjs +399 -0
  69. package/backend/packaged-node-pty.cjs +120 -0
  70. package/backend/preview-session-manager.cjs +89 -0
  71. package/backend/provider-adapters.cjs +492 -0
  72. package/backend/provider-home-configuration.cjs +211 -0
  73. package/backend/provider-session-id.cjs +79 -0
  74. package/backend/provider-session-service.cjs +333 -0
  75. package/backend/qr-share-tickets.cjs +119 -0
  76. package/backend/resource-broadcast-protocol.cjs +50 -0
  77. package/backend/review-diff-router.cjs +202 -0
  78. package/backend/review-diff-service.cjs +1267 -0
  79. package/backend/review-session-router.cjs +93 -0
  80. package/backend/review-session-service.cjs +470 -0
  81. package/backend/review-session-store.cjs +161 -0
  82. package/backend/review-state-router.cjs +132 -0
  83. package/backend/review-state-store.cjs +299 -0
  84. package/backend/run-history-store.cjs +97 -0
  85. package/backend/runtime-dependency-manager.cjs +893 -0
  86. package/backend/runtime-executable-invocation.cjs +37 -0
  87. package/backend/runtime-observation.cjs +96 -0
  88. package/backend/server-process-identity.cjs +70 -0
  89. package/backend/server.cjs +3356 -0
  90. package/backend/session-engine-bridge.cjs +144 -0
  91. package/backend/session-engine-router.cjs +105 -0
  92. package/backend/session-engine.cjs +64 -0
  93. package/backend/session-stream-protocol.cjs +181 -0
  94. package/backend/shell-busy-integration.cjs +442 -0
  95. package/backend/slash-command-discovery.cjs +423 -0
  96. package/backend/storage-layout.cjs +263 -0
  97. package/backend/system-monitor.cjs +96 -0
  98. package/backend/terminal-attach-checkpoint.cjs +73 -0
  99. package/backend/terminal-exit-quiescence.cjs +57 -0
  100. package/backend/{terminal-reducer-flow-control.js → terminal-reducer-flow-control.cjs} +45 -22
  101. package/backend/terminal-runtime-cleanup.cjs +99 -0
  102. package/backend/{terminal-screen-state.js → terminal-screen-state.cjs} +100 -108
  103. package/backend/{terminal-screen-worker-pool.js → terminal-screen-worker-pool.cjs} +89 -79
  104. package/backend/{terminal-screen-worker-thread.js → terminal-screen-worker-thread.cjs} +64 -65
  105. package/backend/terminal-screen-worker.cjs +352 -0
  106. package/backend/terminal-state-serialization.cjs +147 -0
  107. package/backend/{terminal-status.js → terminal-status.cjs} +137 -187
  108. package/backend/theme-manager.cjs +185 -0
  109. package/backend/update-service.cjs +675 -0
  110. package/backend/usage-forecast.cjs +95 -0
  111. package/backend/{usage-history-client.js → usage-history-client.cjs} +144 -94
  112. package/backend/usage-history-smoke.cjs +85 -0
  113. package/backend/usage-history-worker.cjs +60 -0
  114. package/backend/{usage-monitor.js → usage-monitor.cjs} +363 -390
  115. package/backend/workspace-directory.cjs +239 -0
  116. package/backend/workspace-discovery.cjs +271 -0
  117. package/backend/{workspace-file-router.js → workspace-file-router.cjs} +213 -282
  118. package/backend/{workspace-file-service.js → workspace-file-service.cjs} +1139 -1318
  119. package/backend/workspace-root-registry.cjs +192 -0
  120. package/bin/farming +1 -1
  121. package/config/farming.deploy.env.example +1 -1
  122. package/config/farming.install.env.example +1 -1
  123. package/dist/acp/codex-acp-1.1.4.mjs +405 -8
  124. package/dist/assets/App-BCkdpWB6.css +1 -0
  125. package/dist/assets/App-CRjKpaGp.js +697 -0
  126. package/dist/assets/CodeSelect-Ci8sKZPU.js +1 -0
  127. package/dist/assets/CodeSelect-T2eNzrOc.css +1 -0
  128. package/dist/assets/{FileEditorMarkdownPreview-Cs9cLv8a.js → FileEditorMarkdownPreview-V06QHlAg.js} +55 -55
  129. package/dist/assets/FileEditorPane-BX-Wjg9U.js +2 -0
  130. package/dist/assets/IconGlyphs-YhV7tqd4.js +1 -0
  131. package/dist/assets/ProjectFilesSection-BDT2_r0o.js +12 -0
  132. package/dist/assets/ReviewPage-BqAs5sG1.js +3 -0
  133. package/dist/assets/code-dark-B7eM_9G9.css +1 -0
  134. package/dist/assets/{cssMode-Bdg5RpcJ.js → cssMode-ADdAlxTO.js} +1 -1
  135. package/dist/assets/{editor.api2-D74ufkbr.js → editor.api2-BFLAr0YF.js} +162 -162
  136. package/dist/assets/file-icons-BpeqWkIB.js +1 -0
  137. package/dist/assets/{freemarker2-DfMDJfnE.js → freemarker2-C90q5jrp.js} +1 -1
  138. package/dist/assets/{handlebars-BEdPtzMT.js → handlebars-BwUQcEDq.js} +1 -1
  139. package/dist/assets/{html-B1fPb1QD.js → html-1s3uPKfh.js} +1 -1
  140. package/dist/assets/{htmlMode-CY2ZjHxH.js → htmlMode-xtKD99de.js} +1 -1
  141. package/dist/assets/{index-CQNaZc3l.js → index-C8MchCem.js} +3 -3
  142. package/dist/assets/{javascript-BV2OsWA0.js → javascript-Ph7ijyny.js} +1 -1
  143. package/dist/assets/{jsonMode-zd07aeuX.js → jsonMode-D-jGAnFH.js} +1 -1
  144. package/dist/assets/linkComputer-CbGyI_qx.js +1 -0
  145. package/dist/assets/{liquid-BParI24X.js → liquid-0IamE_cI.js} +1 -1
  146. package/dist/assets/{lspLanguageFeatures-BzredkoV.js → lspLanguageFeatures-BxnQunWF.js} +1 -1
  147. package/dist/assets/main-CLKXa6hf.css +1 -0
  148. package/dist/assets/{mdx-vGzUJN98.js → mdx-DdNmfivn.js} +1 -1
  149. package/dist/assets/{monaco.contribution-BmaeBQEc.js → monaco.contribution-B43jsUs1.js} +2 -2
  150. package/dist/assets/{python-CUwdLgsI.js → python-IAt4X5TS.js} +1 -1
  151. package/dist/assets/{razor-BScebUxU.js → razor-DRkFYW0S.js} +1 -1
  152. package/dist/assets/review-C8-oPKXk.css +1 -0
  153. package/dist/assets/{tsMode-DzUAibXK.js → tsMode-_d_4U2gf.js} +1 -1
  154. package/dist/assets/{typescript-D9EOeM7N.js → typescript-C7zcMq9l.js} +1 -1
  155. package/dist/assets/{workers-DAiomYZM.js → workers-D0nOyJzf.js} +1 -1
  156. package/dist/assets/workspace-editor-model-DND_Y-Bj.js +1 -0
  157. package/dist/assets/workspace-editor-monaco-BPWhgcIB.js +4 -0
  158. package/dist/assets/workspace-editor-monaco-N641qPjX.js +1 -0
  159. package/dist/assets/workspace-view-state-k1bTlJ_Z.js +13 -0
  160. package/dist/assets/{xml-wWl1dhkQ.js → xml-BR-HvxT8.js} +1 -1
  161. package/dist/assets/{yaml-Dc2WDaOZ.js → yaml-YtFQwh4q.js} +1 -1
  162. package/dist/index.html +2 -1
  163. package/extensions/browser/backend/agent-browser-runtime.cjs +1040 -0
  164. package/extensions/browser/backend/agent-capability.cjs +85 -0
  165. package/extensions/browser/backend/browser-mcp-server.cjs +655 -0
  166. package/extensions/browser/backend/browser-resource-manager.cjs +1486 -0
  167. package/extensions/browser/backend/browser-resource-store.cjs +228 -0
  168. package/extensions/browser/backend/browser-router.cjs +203 -0
  169. package/extensions/browser/backend/executable-discovery.cjs +248 -0
  170. package/extensions/browser/backend/farming-browser-client.cjs +116 -0
  171. package/extensions/browser/backend/index.cjs +32 -0
  172. package/extensions/browser/backend/managed-chromium-installer.cjs +833 -0
  173. package/extensions/browser/bin/farming-browser +16 -7
  174. package/extensions/computer/backend/agent-capability.cjs +72 -0
  175. package/extensions/computer/backend/computer-constants.cjs +59 -0
  176. package/extensions/computer/backend/computer-mcp-server.cjs +182 -0
  177. package/extensions/computer/backend/computer-resource-manager.cjs +1219 -0
  178. package/extensions/computer/backend/computer-resource-store.cjs +190 -0
  179. package/extensions/computer/backend/computer-router.cjs +257 -0
  180. package/extensions/computer/backend/cua-tools.json +3067 -0
  181. package/extensions/computer/backend/farming-computer-client.cjs +84 -0
  182. package/extensions/computer/backend/index.cjs +35 -0
  183. package/extensions/computer/backend/isolated-browser-provider.cjs +157 -0
  184. package/extensions/computer/bin/farming-computer +131 -0
  185. package/frontend/farming-net/app.js +255 -262
  186. package/frontend/ghostty-loader.js +24 -20
  187. package/frontend/reading-anchor.js +210 -193
  188. package/frontend/runtime-paths.js +24 -26
  189. package/frontend/session-bridge.js +90 -111
  190. package/frontend/session-modal-bridge.js +184 -302
  191. package/frontend/skin-bridge.js +20 -23
  192. package/frontend/skins/crt/app.js +7686 -7901
  193. package/frontend/skins/crt/effects.js +7 -6
  194. package/frontend/skins/crt/index.html +6 -6
  195. package/frontend/skins/crt/styles/markdown.css +4 -4
  196. package/frontend/terminal-bridge.js +174 -181
  197. package/frontend/terminal-replay.js +320 -345
  198. package/frontend/theme-loader.js +55 -71
  199. package/index.html +1 -0
  200. package/package.json +64 -37
  201. package/shared/backend-connection-status.js +38 -69
  202. package/shared/browser-protocol.js +235 -110
  203. package/shared/websocket-liveness.js +40 -0
  204. package/backend/acp/packaged-claude-acp.js +0 -13
  205. package/backend/acp/packaged-codex-acp.js +0 -13
  206. package/backend/acp/patch-decisions.js +0 -153
  207. package/backend/acp/permission-security.js +0 -108
  208. package/backend/acp-session-state.js +0 -644
  209. package/backend/acp-transcript.js +0 -512
  210. package/backend/agent-env.js +0 -250
  211. package/backend/agent-json-stream.js +0 -268
  212. package/backend/agent-lifecycle-journal.js +0 -133
  213. package/backend/agent-memory-report.js +0 -583
  214. package/backend/agent-order.js +0 -166
  215. package/backend/agent-provider-session.js +0 -76
  216. package/backend/agent-runtime-binding.js +0 -174
  217. package/backend/agent-session-history.js +0 -953
  218. package/backend/async-cache.js +0 -94
  219. package/backend/atomic-json-store.js +0 -37
  220. package/backend/auth.js +0 -251
  221. package/backend/bundle-update-helper.js +0 -113
  222. package/backend/chat-runtime.js +0 -28
  223. package/backend/claude-settings.js +0 -121
  224. package/backend/cli-agents.js +0 -486
  225. package/backend/codex-context-window.js +0 -189
  226. package/backend/codex-models.js +0 -226
  227. package/backend/codex-rollout-follower.js +0 -326
  228. package/backend/codex-session-archive.js +0 -65
  229. package/backend/codex-session-history.js +0 -647
  230. package/backend/codex-transcript-sanitizer.js +0 -115
  231. package/backend/codex-transcript.js +0 -2564
  232. package/backend/command-runner-child.js +0 -67
  233. package/backend/config-manager.js +0 -1133
  234. package/backend/control-api.js +0 -455
  235. package/backend/executable-discovery.js +0 -216
  236. package/backend/farming-agent-bootstrap.js +0 -49
  237. package/backend/farming-cli.js +0 -457
  238. package/backend/farming-net-pass.js +0 -285
  239. package/backend/farming-net-registry.js +0 -112
  240. package/backend/farming-net-server.js +0 -298
  241. package/backend/farming-session-store.js +0 -677
  242. package/backend/git-worktree-info.js +0 -181
  243. package/backend/haiku-token.js +0 -772
  244. package/backend/index-html.js +0 -62
  245. package/backend/input-parts.js +0 -54
  246. package/backend/input-routing.js +0 -24
  247. package/backend/json-cli-runtime.js +0 -275
  248. package/backend/main-agent-skills.js +0 -305
  249. package/backend/main-page-session.js +0 -122
  250. package/backend/native-pty-controller-generation.js +0 -106
  251. package/backend/native-pty-host-identity.js +0 -86
  252. package/backend/native-pty-host-path.js +0 -42
  253. package/backend/native-session-engine.js +0 -268
  254. package/backend/network.js +0 -23
  255. package/backend/npm-update-helper.js +0 -374
  256. package/backend/packaged-node-pty.js +0 -99
  257. package/backend/preview-session-manager.js +0 -68
  258. package/backend/provider-adapters.js +0 -399
  259. package/backend/provider-session-id.js +0 -47
  260. package/backend/provider-session-service.js +0 -345
  261. package/backend/qr-share-tickets.js +0 -100
  262. package/backend/review-diff-router.js +0 -180
  263. package/backend/review-diff-service.js +0 -1270
  264. package/backend/review-session-router.js +0 -74
  265. package/backend/review-session-service.js +0 -446
  266. package/backend/review-session-store.js +0 -151
  267. package/backend/review-state-router.js +0 -116
  268. package/backend/review-state-store.js +0 -286
  269. package/backend/run-history-store.js +0 -67
  270. package/backend/runtime-dependency-manager.js +0 -759
  271. package/backend/runtime-executable-invocation.js +0 -20
  272. package/backend/runtime-observation.js +0 -81
  273. package/backend/server-process-identity.js +0 -47
  274. package/backend/server.js +0 -3267
  275. package/backend/session-engine-bridge.js +0 -129
  276. package/backend/session-engine-router.js +0 -66
  277. package/backend/session-engine.js +0 -39
  278. package/backend/session-stream-protocol.js +0 -185
  279. package/backend/shell-busy-integration.js +0 -472
  280. package/backend/slash-command-discovery.js +0 -433
  281. package/backend/storage-layout.js +0 -190
  282. package/backend/system-monitor.js +0 -77
  283. package/backend/terminal-attach-checkpoint.js +0 -74
  284. package/backend/terminal-exit-quiescence.js +0 -39
  285. package/backend/terminal-runtime-cleanup.js +0 -80
  286. package/backend/terminal-screen-worker.js +0 -337
  287. package/backend/terminal-state-serialization.js +0 -127
  288. package/backend/theme-manager.js +0 -163
  289. package/backend/update-service.js +0 -1545
  290. package/backend/usage-forecast.js +0 -86
  291. package/backend/usage-history-scanner.ts +0 -2718
  292. package/backend/usage-history-smoke.js +0 -64
  293. package/backend/usage-history-worker.js +0 -34
  294. package/backend/workspace-directory.js +0 -232
  295. package/backend/workspace-discovery.js +0 -311
  296. package/backend/workspace-root-registry.js +0 -166
  297. package/dist/assets/App-BKNUILuL.css +0 -1
  298. package/dist/assets/App-Ex2oVVXO.js +0 -693
  299. package/dist/assets/FileEditorPane-CNtuGjQ3.js +0 -2
  300. package/dist/assets/IconGlyphs-BClgag6E.js +0 -1
  301. package/dist/assets/ProjectFilesSection-Cf2BEcAN.js +0 -12
  302. package/dist/assets/ReviewPage-BRuxFaH5.js +0 -3
  303. package/dist/assets/code-dark-BEZTQwYZ.css +0 -1
  304. package/dist/assets/file-icons-BOlcJ0ZF.js +0 -1
  305. package/dist/assets/main-DK9Sy7-c.css +0 -1
  306. package/dist/assets/review-BcnwQtJv.css +0 -1
  307. package/dist/assets/workspace-editor-model-CSGCl21x.js +0 -1
  308. package/dist/assets/workspace-editor-monaco-CJNwbpCy.js +0 -4
  309. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +0 -1
  310. package/dist/assets/workspace-view-state-BjKPYp0E.js +0 -7
  311. package/extensions/browser/backend/agent-browser-runtime.js +0 -1041
  312. package/extensions/browser/backend/agent-capability.js +0 -55
  313. package/extensions/browser/backend/browser-mcp-server.js +0 -621
  314. package/extensions/browser/backend/browser-resource-manager.js +0 -1309
  315. package/extensions/browser/backend/browser-resource-store.js +0 -190
  316. package/extensions/browser/backend/browser-router.js +0 -133
  317. package/extensions/browser/backend/executable-discovery.js +0 -224
  318. package/extensions/browser/backend/farming-browser-client.js +0 -83
  319. package/extensions/browser/backend/index.js +0 -7
  320. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +0 -360
  321. package/extensions/browser/frontend/BrowserViewer.tsx +0 -500
  322. package/extensions/browser/frontend/browser-resource-state.ts +0 -60
  323. package/extensions/browser/frontend/browser.css +0 -567
  324. package/extensions/browser/frontend/types.ts +0 -31
  325. package/extensions/browser/frontend/useBrowserResources.ts +0 -158
  326. package/frontend/vendor/ghostty-web/index.d.ts +0 -1842
  327. package/shared/backend-connection-status.d.ts +0 -37
  328. package/shared/browser-protocol.d.ts +0 -5
  329. /package/dist/assets/{api-w2x5QfxB.js → api-ByCV9MEr.js} +0 -0
  330. /package/dist/assets/{core-vPkV8P_m.js → core-Czwdx12k.js} +0 -0
  331. /package/dist/assets/{fsharp-DNZNZyEE.js → fsharp-BjPpkoJe.js} +0 -0
  332. /package/dist/assets/{go-_kPJjNc_.js → go-BkIDmSUP.js} +0 -0
  333. /package/dist/assets/{graphql-4HCNVt5g.js → graphql-CCJTpqi1.js} +0 -0
  334. /package/dist/assets/{hcl-C87ymY0o.js → hcl-s4z8lUFg.js} +0 -0
  335. /package/dist/assets/{ini-BYrrGNTc.js → ini-CyUW1I7Z.js} +0 -0
  336. /package/dist/assets/{java-B211rJbk.js → java-CFCcGRiM.js} +0 -0
  337. /package/dist/assets/{julia-DVPsRtBm.js → julia-DQyH_EYb.js} +0 -0
  338. /package/dist/assets/{kotlin-BYyNerjp.js → kotlin-B8hA5O51.js} +0 -0
  339. /package/dist/assets/{less-EC9UblGE.js → less-DaVdaas0.js} +0 -0
  340. /package/dist/assets/{lexon-B1yuUdqW.js → lexon-DLBhPd3y.js} +0 -0
  341. /package/dist/assets/{lua-Bo6GFzxT.js → lua-gB1c_cif.js} +0 -0
  342. /package/dist/assets/{m3-C-Y9cBVI.js → m3-C2T3z3gB.js} +0 -0
  343. /package/dist/assets/{markdown-BHu-wLbe.js → markdown-CKq-Euj1.js} +0 -0
  344. /package/dist/assets/{mips-Dkds26rX.js → mips-Bcs5qIDX.js} +0 -0
  345. /package/dist/assets/{msdax-PU4g-8_g.js → msdax-CBP48FHF.js} +0 -0
  346. /package/dist/assets/{mysql-fPpuGpS7.js → mysql-c7tiPcFj.js} +0 -0
  347. /package/dist/assets/{objective-c-Wy1e5JUf.js → objective-c-D4BZkeEV.js} +0 -0
  348. /package/dist/assets/{pascal-BIXwvrqU.js → pascal-CJDZg0MN.js} +0 -0
  349. /package/dist/assets/{pascaligo-DwR0AtL_.js → pascaligo-Bygi8Ynl.js} +0 -0
  350. /package/dist/assets/{perl-CGHAl8my.js → perl-aS8bkf7E.js} +0 -0
  351. /package/dist/assets/{pgsql-B91M73E2.js → pgsql-CzNAYFYl.js} +0 -0
  352. /package/dist/assets/{php-C70ttrCx.js → php-DOtoEFTT.js} +0 -0
  353. /package/dist/assets/{pla-BNrXaRh0.js → pla-DugRnao1.js} +0 -0
  354. /package/dist/assets/{postiats-Dv2IAZR9.js → postiats-DsXwusKh.js} +0 -0
  355. /package/dist/assets/{powerquery-CkY5Wq7J.js → powerquery-DL3o72oD.js} +0 -0
  356. /package/dist/assets/{powershell-uGIYLU3C.js → powershell-C1qHekys.js} +0 -0
  357. /package/dist/assets/{protobuf-CavfmJu-.js → protobuf-DHgsc-cH.js} +0 -0
  358. /package/dist/assets/{pug-Drqnyitx.js → pug-FZXlE_Iu.js} +0 -0
  359. /package/dist/assets/{qrcode-7uGsTEmi.js → qrcode-DJDTdjL7.js} +0 -0
  360. /package/dist/assets/{qsharp-C7yF-Dti.js → qsharp-bLyhI3DT.js} +0 -0
  361. /package/dist/assets/{r-HXLGP0_-.js → r-DjFGXbAa.js} +0 -0
  362. /package/dist/assets/{redis-C4y6mf2p.js → redis-DSoZ_V6u.js} +0 -0
  363. /package/dist/assets/{redshift-DdQrFPrv.js → redshift-CzkdCsrg.js} +0 -0
  364. /package/dist/assets/{restructuredtext-CBrf2PNj.js → restructuredtext-B_XPrGCQ.js} +0 -0
  365. /package/dist/assets/{ruby-CVtMoX2B.js → ruby-Cry2RneJ.js} +0 -0
  366. /package/dist/assets/{rust-Ckq4sbMP.js → rust-CWzdbFq1.js} +0 -0
  367. /package/dist/assets/{sb-er66OpE3.js → sb-DmofU59h.js} +0 -0
  368. /package/dist/assets/{scala-BdWZz-hH.js → scala-BUC2l-vN.js} +0 -0
  369. /package/dist/assets/{scheme-kTV_s2kP.js → scheme-CNIB_72f.js} +0 -0
  370. /package/dist/assets/{scss-Bymy2VJQ.js → scss-DcL4-NbX.js} +0 -0
  371. /package/dist/assets/{shell-bwEP1LQ1.js → shell-CgAGvSSA.js} +0 -0
  372. /package/dist/assets/{solidity-DCddeBgL.js → solidity-DSkt6O_P.js} +0 -0
  373. /package/dist/assets/{sophia-BMN3AQig.js → sophia--5ytcjDN.js} +0 -0
  374. /package/dist/assets/{sparql-CSXOLjzi.js → sparql-C1Srr7cD.js} +0 -0
  375. /package/dist/assets/{sql-COg8ewmK.js → sql-2k_O5fj7.js} +0 -0
  376. /package/dist/assets/{st-CIaZtBS_.js → st-DCCCPqfz.js} +0 -0
  377. /package/dist/assets/{swift-DXiOhUHu.js → swift-Dd8mAfOt.js} +0 -0
  378. /package/dist/assets/{systemverilog-BTDE6Dr2.js → systemverilog-B0-aZYXu.js} +0 -0
  379. /package/dist/assets/{tcl-B0zy9NvV.js → tcl-CZAZwPVX.js} +0 -0
  380. /package/dist/assets/{twig-D594l4Z1.js → twig-D-Ag0tdS.js} +0 -0
  381. /package/dist/assets/{typespec-XKVjaPqo.js → typespec-B7kTFp0Z.js} +0 -0
  382. /package/dist/assets/{useWorkspaceMenuKeyboard-Bpy_s69Z.js → useWorkspaceMenuKeyboard-uYZpDxhU.js} +0 -0
  383. /package/dist/assets/{vb-B2wYoKRV.js → vb-20w1JPum.js} +0 -0
  384. /package/dist/assets/{wgsl-D0Yf2LbK.js → wgsl-CLJfqFt0.js} +0 -0
  385. /package/dist/assets/{workspace-working-copy-C3FSR3N6.js → workspace-working-copy-Cfz07cOd.js} +0 -0
package/backend/server.js DELETED
@@ -1,3267 +0,0 @@
1
- const express = require('express');
2
- const WebSocket = require('ws');
3
- const http = require('http');
4
- const path = require('path');
5
- const fs = require('fs');
6
- const os = require('os');
7
- const crypto = require('crypto');
8
- const { execFile } = require('child_process');
9
- const { promisify } = require('util');
10
- const { URLSearchParams, pathToFileURL } = require('url');
11
- const AgentManager = require('./agent-manager');
12
- const { runtimeKind } = require('./agent-runtime-binding');
13
- const ConfigManager = require('./config-manager');
14
- const ThemeManager = require('./theme-manager');
15
- const TokenAuth = require('./auth');
16
- const { getLocalIPs, getPrimaryLocalIP } = require('./network');
17
- const { listAvailableAgents, resolveCompatibleCodexExecutable } = require('./executable-discovery');
18
- const { readClaudeSettingsSummary } = require('./claude-settings');
19
- const { listCodexModelOptions } = require('./codex-models');
20
- const { listCodexSessions } = require('./codex-session-history');
21
- const {
22
- buildAgentSessionResumeCommand,
23
- findAgentSession,
24
- isSafeSessionId,
25
- listAgentSessions,
26
- normalizeProvider,
27
- paginateAgentSessions,
28
- resolveCodexResumeModelProvider,
29
- searchAgentSessions,
30
- } = require('./agent-session-history');
31
- const {
32
- findActiveAgentClaimingSession,
33
- mainPageAgentSessionKey,
34
- mainPageAgentSessionFromKey,
35
- mainPageAgentSessionsToAutoResume,
36
- resumedAgentSource,
37
- } = require('./main-page-session');
38
- const { discoverAgentWorkspaces } = require('./workspace-discovery');
39
- const { inspectGitWorktree } = require('./git-worktree-info');
40
- const { createWorkspaceDirectoryRouter } = require('./workspace-directory');
41
- const { createControlRouter } = require('./control-api');
42
- const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
43
- const { createWorkspaceFileRouter, resolveWorkspaceRoot } = require('./workspace-file-router');
44
- const { WorkspaceRootRegistry, rootIdForPath } = require('./workspace-root-registry');
45
- const {
46
- BrowserResourceManager,
47
- createBrowserRouter,
48
- } = require('../extensions/browser/backend');
49
- const { UsageMonitor } = require('./usage-monitor');
50
- const { CodexContextWindowReader } = require('./codex-context-window');
51
- const { DEFAULT_MAX_TURNS: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS, readCodexTranscript } = require('./codex-transcript');
52
- const { AsyncCache } = require('./async-cache');
53
- const { getMainAgentSkillsCatalog } = require('./main-agent-skills');
54
- const { discoverAgentExtensions, discoverSlashCommands } = require('./slash-command-discovery');
55
- const { FarmingUpdateService } = require('./update-service');
56
- const { inputPartsFromMessage } = require('./input-parts');
57
- const { cleanupTerminalRuntime } = require('./terminal-runtime-cleanup');
58
- const { QrShareTicketStore, SHARE_TICKET_TTL_MS } = require('./qr-share-tickets');
59
- const { ReviewStateStore } = require('./review-state-store');
60
- const { createReviewStateRouter } = require('./review-state-router');
61
- const { ReviewDiffService } = require('./review-diff-service');
62
- const { createReviewDiffRouter } = require('./review-diff-router');
63
- const { ReviewSessionStore } = require('./review-session-store');
64
- const { ReviewSessionService } = require('./review-session-service');
65
- const { createReviewSessionRouter } = require('./review-session-router');
66
- const {
67
- applyIndexHtmlAppearance,
68
- normalizeBasePath,
69
- routePath,
70
- rewriteIndexHtmlForBasePath,
71
- appendIndexHtmlAssetToken,
72
- } = require('./index-html');
73
- const { decodeAcpTranscriptMedia } = require('./acp-transcript');
74
- const {
75
- coalesceSessionStream,
76
- deliverSessionStreamToClients,
77
- shouldBroadcastSessionStreamImmediately,
78
- } = require('./session-stream-protocol');
79
- const {
80
- MIN_PROTOCOL_VERSION,
81
- PROTOCOL_VERSION,
82
- protocolCompatible,
83
- sanitizeAgentUpdatePatch,
84
- validateClientMessage,
85
- } = require('../shared/browser-protocol');
86
-
87
- const BASE_PATH = normalizeBasePath(process.env.FARMING_BASE_PATH || '/');
88
- const PORT = process.env.PORT || 3000;
89
- const tokenAuth = new TokenAuth({ basePath: BASE_PATH || '/' });
90
- const authEnabled = tokenAuth.isEnabled();
91
- const WS_PATH = routePath(BASE_PATH, '/ws');
92
- const encodeCookieToken = TokenAuth.encodeCookieToken;
93
- const MAX_CODEX_TRANSCRIPT_TURNS = 1000;
94
- const INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS = 3_000;
95
- const execFileAsync = promisify(execFile);
96
-
97
- const app = express();
98
- const server = http.createServer(app);
99
- const wss = new WebSocket.Server({ server });
100
-
101
- const configManager = new ConfigManager();
102
- configManager.init();
103
-
104
- function resolveCliBinDir() {
105
- if (process.env.FARMING_CLI_BIN_DIR) {
106
- return process.env.FARMING_CLI_BIN_DIR;
107
- }
108
- if (process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1') {
109
- return path.dirname(process.execPath);
110
- }
111
- return path.join(__dirname, '..', 'bin');
112
- }
113
-
114
- const agentManager = new AgentManager(configManager, {
115
- controlUrl: `http://127.0.0.1:${PORT}${BASE_PATH}`,
116
- tokenFile: tokenAuth.getTokenFile(),
117
- authDisabled: !authEnabled,
118
- cliBinDir: resolveCliBinDir(),
119
- });
120
-
121
- async function requireAgentRecoveryForHttp(res) {
122
- try {
123
- await agentManager.whenRecovered();
124
- return true;
125
- } catch (error) {
126
- res.status(503).json({
127
- error: error?.message || 'Agent lifecycle recovery is unavailable',
128
- retryable: true,
129
- });
130
- return false;
131
- }
132
- }
133
-
134
- const themeManager = new ThemeManager({ configDir: configManager.farmingDir });
135
- const workspaceFileService = new WorkspaceFileService();
136
- const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
137
- const browserResourceManager = new BrowserResourceManager({
138
- configDir: configManager.farmingDir,
139
- isEnabled: () => configManager.getSettings().browserExtensionEnabled === true,
140
- getBrowserSettings: () => configManager.getSettings(),
141
- });
142
- const browserRuntimeRecoveryPromise = browserResourceManager.init().catch(error => {
143
- console.warn('Failed to recover Browser runtimes:', error?.message || error);
144
- return null;
145
- });
146
- const updateService = new FarmingUpdateService({
147
- rootDir: path.join(__dirname, '..'),
148
- configDir: configManager.farmingDir,
149
- platform: process.platform,
150
- arch: process.arch,
151
- packagedRuntime: Boolean(process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1'),
152
- getUpdateUrl: () => configManager.getSettings().updateUrl || '',
153
- });
154
- const usageMonitor = new UsageMonitor({
155
- agentManager,
156
- configDir: configManager.farmingDir,
157
- getProviderHomes: configuredProviderHomes,
158
- });
159
- const codexContextWindowReader = new CodexContextWindowReader();
160
- const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
161
- ttlMs: 15_000,
162
- staleMs: 2 * 60_000,
163
- });
164
- const codexModelOptionsCache = new AsyncCache(() => listCodexModelOptions(), {
165
- ttlMs: 5 * 60_000,
166
- staleMs: 5 * 60_000,
167
- });
168
- function configuredProviderHomes() {
169
- const settings = configManager.getSettings();
170
- const agentHomes = settings.agentHomes && typeof settings.agentHomes === 'object' ? settings.agentHomes : {};
171
- const result = {};
172
- for (const [provider, homes] of Object.entries(agentHomes)) {
173
- if (!Array.isArray(homes)) continue;
174
- result[provider] = homes.map(home => ({
175
- id: String(home.id || 'default'),
176
- path: configManager.expandWorkspacePath(String(home.path || '')),
177
- })).filter(home => home.id && home.path);
178
- }
179
- return result;
180
- }
181
-
182
- const agentSessionsCache = new AsyncCache(() => {
183
- return listAgentSessions({
184
- limit: 5000,
185
- providerLimit: 5000,
186
- scanLimit: 5000,
187
- providerHomes: configuredProviderHomes(),
188
- });
189
- }, {
190
- ttlMs: 30_000,
191
- staleMs: 5 * 60_000,
192
- });
193
-
194
- function withSearchTimeout(promise, timeoutMs) {
195
- let timer;
196
- const timeout = new Promise((_, reject) => {
197
- timer = setTimeout(() => {
198
- const error = new Error('Agent search timed out');
199
- error.code = 'ETIMEDOUT';
200
- reject(error);
201
- }, timeoutMs);
202
- });
203
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
204
- }
205
-
206
- const qrShareTickets = new QrShareTicketStore({ ttlMs: SHARE_TICKET_TTL_MS });
207
- const reviewStateStore = new ReviewStateStore(configManager.farmingDir, {
208
- seedReviews: {
209
- 'review-fixture-553987': {
210
- patchsets: {
211
- 'Patchset 20': { reviewedPaths: ['clis/dataflow.py', 'clis/fetch_instance_log.py'], revision: 0 },
212
- 'Patchset 19': { reviewedPaths: ['clis/fetch_instance_log.py'], revision: 0 },
213
- },
214
- },
215
- },
216
- });
217
- const reviewDiffService = new ReviewDiffService(agentManager, workspaceFileService);
218
- const reviewSessionStore = new ReviewSessionStore(configManager.farmingDir);
219
- const reviewSessionService = new ReviewSessionService(workspaceFileService, reviewSessionStore, reviewStateStore, {
220
- resolveAgentRoot: agentId => agentManager.getAgentWorkspaceRoot(agentId),
221
- resolveAcpReviewChanges: (agentId, itemIds) => agentManager.getAcpReviewChanges(agentId, itemIds),
222
- });
223
- const workspaceDiscoveryCache = new AsyncCache((key) => {
224
- const request = JSON.parse(key);
225
- return discoverAgentWorkspaces({
226
- limit: request.limit,
227
- agent: request.agent,
228
- });
229
- }, {
230
- ttlMs: 30_000,
231
- staleMs: 2 * 60_000,
232
- });
233
-
234
- const frontendDir = path.join(__dirname, '../frontend');
235
- const crtFrontendDir = path.join(frontendDir, 'skins', 'crt');
236
- const distDir = path.join(__dirname, '../dist');
237
- const staticAppDir = fs.existsSync(distDir) ? distDir : frontendDir;
238
- const xtermBrowserEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'xterm', 'lib', 'xterm.js');
239
- const xtermFitEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'addon-fit', 'lib', 'addon-fit.js');
240
- const xtermWebglEntryPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'addon-webgl', 'lib', 'addon-webgl.js');
241
- const xtermCssPath = path.join(__dirname, '..', 'node_modules', '@xterm', 'xterm', 'css', 'xterm.css');
242
- const materialIconDir = path.join(__dirname, '..', 'node_modules', 'material-icon-theme', 'icons');
243
-
244
- function getAvailableAgentsForRequest() {
245
- if (process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
246
- return [
247
- {
248
- name: 'codex',
249
- command: 'codex',
250
- description: 'Codex CLI - OpenAI coding assistant',
251
- category: 'coding',
252
- supported: true,
253
- interactive: true,
254
- },
255
- {
256
- name: 'claude',
257
- command: 'claude',
258
- description: 'Claude CLI - Anthropic assistant',
259
- category: 'coding',
260
- supported: true,
261
- interactive: true,
262
- },
263
- {
264
- name: 'opencode',
265
- command: 'opencode',
266
- description: 'OpenCode - AI coding assistant',
267
- category: 'coding',
268
- supported: true,
269
- interactive: true,
270
- },
271
- {
272
- name: 'qoder',
273
- command: 'qodercli',
274
- description: 'Qoder - AI coding assistant',
275
- category: 'coding',
276
- supported: true,
277
- interactive: true,
278
- },
279
- {
280
- name: 'bash',
281
- command: 'bash',
282
- description: 'Shell session',
283
- category: 'shell',
284
- supported: true,
285
- interactive: true,
286
- },
287
- {
288
- name: 'zsh',
289
- command: 'zsh',
290
- description: 'Z shell',
291
- category: 'shell',
292
- supported: true,
293
- interactive: true,
294
- },
295
- ];
296
- }
297
-
298
- const shellEnv = agentManager.resolveAgentShellEnv('', { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS });
299
- const pathEnv = typeof shellEnv?.PATH === 'string' && shellEnv.PATH.trim()
300
- ? shellEnv.PATH
301
- : (process.env.PATH || '');
302
- return listAvailableAgents(pathEnv);
303
- }
304
-
305
- function normalizeWorkspaceCompletionInput(value) {
306
- const raw = typeof value === 'string' ? value.trim() : '';
307
- if (!raw) {
308
- return { raw: '', parent: os.homedir(), prefix: '', displayParent: '~', explicitDirectory: false };
309
- }
310
-
311
- const home = os.homedir();
312
- const expanded = raw === '~' ? home : raw.startsWith('~/') ? path.join(home, raw.slice(2)) : raw;
313
- const explicitDirectory = raw.endsWith(path.sep) || raw === '~';
314
- const parent = explicitDirectory ? expanded : path.dirname(expanded);
315
- const prefix = explicitDirectory ? '' : path.basename(expanded);
316
- const trimTrailingSeparator = (input) => {
317
- let next = input;
318
- while (next.length > 1 && next.endsWith(path.sep)) next = next.slice(0, -1);
319
- return next;
320
- };
321
- const displayParent = explicitDirectory
322
- ? trimTrailingSeparator(raw)
323
- : trimTrailingSeparator(raw.slice(0, raw.length - prefix.length));
324
-
325
- return {
326
- raw,
327
- parent: parent || path.sep,
328
- prefix,
329
- displayParent: displayParent || (path.isAbsolute(raw) ? path.sep : ''),
330
- explicitDirectory,
331
- };
332
- }
333
-
334
- async function listWorkspacePathCompletions(partialPath, limit = 12) {
335
- const query = normalizeWorkspaceCompletionInput(partialPath);
336
- const entries = await fs.promises.readdir(query.parent, { withFileTypes: true });
337
- const normalizedPrefix = query.prefix.toLowerCase();
338
- const maxResults = Math.max(1, Math.min(Number(limit) || 12, 100));
339
-
340
- return entries
341
- .filter(entry => entry.isDirectory())
342
- .filter(entry => normalizedPrefix.startsWith('.') || !entry.name.startsWith('.'))
343
- .filter(entry => !normalizedPrefix || entry.name.toLowerCase().startsWith(normalizedPrefix))
344
- .sort((a, b) => a.name.localeCompare(b.name))
345
- .slice(0, maxResults)
346
- .map(entry => {
347
- const fullPath = path.join(query.parent, entry.name);
348
- const displayPath = query.raw.startsWith('~')
349
- ? path.join(query.displayParent || '~', entry.name)
350
- : fullPath;
351
- return {
352
- name: entry.name,
353
- path: `${displayPath}${path.sep}`,
354
- };
355
- });
356
- }
357
-
358
- // iOS can fetch installed-web-app metadata outside the authenticated page
359
- // request, without preserving its cookie or token query. These files contain
360
- // only public product artwork and must remain available to that fetcher.
361
- const publicProductAssetsDir = path.join(staticAppDir, 'farming-2');
362
- app.use(routePath(BASE_PATH, '/farming-2'), express.static(publicProductAssetsDir, { index: false }));
363
-
364
- const sendPublicProductAsset = (filename) => (_req, res) => {
365
- res.sendFile(path.join(publicProductAssetsDir, filename));
366
- };
367
-
368
- app.get(['/favicon.ico', routePath(BASE_PATH, '/favicon.ico')], sendPublicProductAsset('favicon-v2.ico'));
369
- app.get([
370
- '/apple-touch-icon.png',
371
- '/apple-touch-icon-precomposed.png',
372
- '/apple-touch-icon-180x180.png',
373
- routePath(BASE_PATH, '/apple-touch-icon.png'),
374
- routePath(BASE_PATH, '/apple-touch-icon-precomposed.png'),
375
- routePath(BASE_PATH, '/apple-touch-icon-180x180.png'),
376
- ], sendPublicProductAsset('app-icon-v2-180.png'));
377
-
378
- app.get(routePath(BASE_PATH, '/j/:code'), (req, res) => {
379
- if (req.method === 'HEAD') {
380
- res.status(204).end();
381
- return;
382
- }
383
-
384
- const ticket = qrShareTickets.consume(req.params.code);
385
- if (!ticket || (authEnabled && !tokenAuth.verify(ticket.token))) {
386
- res.status(410).send('Farming share link expired.');
387
- return;
388
- }
389
-
390
- if (authEnabled) {
391
- res.setHeader(
392
- 'Set-Cookie',
393
- `farming_token=${encodeCookieToken(ticket.token)}; Path=/; HttpOnly; SameSite=Lax`,
394
- );
395
- }
396
- res.redirect(302, entryPathWithQuery(ticket.targetQuery));
397
- });
398
-
399
- // Token authentication middleware (before static files)
400
- app.use(tokenAuth.middleware());
401
-
402
- // Auth status endpoint (allowed without authentication via middleware)
403
- app.get(routePath(BASE_PATH, '/api/auth/status'), (req, res) => {
404
- res.json({ authRequired: authEnabled });
405
- });
406
-
407
- function absoluteClientUrl(req, urlPath) {
408
- const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim();
409
- const protocol = forwardedProto || req.protocol || 'http';
410
- const forwardedHost = String(req.headers['x-forwarded-host'] || '').split(',')[0].trim();
411
- const host = forwardedHost || req.headers.host || `127.0.0.1:${PORT}`;
412
- return `${protocol}://${host}${urlPath}`;
413
- }
414
-
415
- function shareTargetPositiveInteger(value) {
416
- const number = Number(value);
417
- return Number.isInteger(number) && number > 0 ? String(number) : '';
418
- }
419
-
420
- function shareTargetString(value, maxLength) {
421
- const string = String(value || '').trim();
422
- if (!string || string.length > maxLength || string.includes('\0')) return '';
423
- return string;
424
- }
425
-
426
- function shareTargetQueryFromBody(body) {
427
- const target = body && typeof body === 'object' ? body.target : null;
428
- if (!target || typeof target !== 'object') return '';
429
-
430
- const kind = target.kind === 'file' ? 'file' : target.kind === 'folder' ? 'folder' : target.kind === 'agent' ? 'agent' : '';
431
- const agentId = shareTargetString(target.agentId, 160);
432
- const absolutePath = shareTargetString(target.absolutePath, 2048);
433
- const projectLabel = shareTargetString(target.projectLabel, 160);
434
- const readingAnchor = shareTargetString(target.readingAnchor, 1800);
435
- if (!kind || kind === 'agent' && !agentId || kind !== 'agent' && !absolutePath && !agentId && !projectLabel) return '';
436
-
437
- const params = new URLSearchParams();
438
- params.set('ftarget', kind);
439
- if (agentId) params.set('agent', agentId);
440
- if (kind === 'agent' && readingAnchor && /^[A-Za-z0-9_-]+$/.test(readingAnchor)) params.set('fra', readingAnchor);
441
- if (absolutePath) params.set('path', absolutePath);
442
- if (projectLabel) params.set('project', projectLabel);
443
-
444
- if (kind === 'folder') {
445
- const folderPath = shareTargetString(target.folderPath, 2048);
446
- if (!absolutePath && !folderPath) return '';
447
- if (folderPath) params.set('folder', folderPath);
448
- } else if (kind === 'file') {
449
- const filePath = shareTargetString(target.filePath, 2048);
450
- if (!absolutePath && !filePath) return '';
451
- if (filePath) params.set('file', filePath);
452
- if (target.view === 'diff') params.set('view', 'diff');
453
- const line = shareTargetPositiveInteger(target.lineNumber);
454
- const column = shareTargetPositiveInteger(target.column);
455
- const endColumn = shareTargetPositiveInteger(target.endColumn);
456
- if (line) params.set('line', line);
457
- if (column) params.set('column', column);
458
- if (endColumn) params.set('endColumn', endColumn);
459
- }
460
-
461
- if (absolutePath) {
462
- const absoluteParams = new URLSearchParams(params);
463
- absoluteParams.delete('agent');
464
- absoluteParams.delete(kind === 'folder' ? 'folder' : 'file');
465
- if (absoluteParams.toString().length <= 1800) return absoluteParams.toString();
466
- params.delete('path');
467
- }
468
-
469
- return params.toString();
470
- }
471
-
472
- function entryPathWithQuery(query = '', options = {}) {
473
- const entryPath = BASE_PATH || '/';
474
- const params = new URLSearchParams(query || '');
475
- if (options.includeToken && authEnabled) {
476
- params.set('token', tokenAuth.getToken());
477
- }
478
- const queryString = params.toString();
479
- return queryString ? `${entryPath}?${queryString}` : entryPath;
480
- }
481
-
482
- function entryPathWithToken(targetQuery = '') {
483
- return entryPathWithQuery(targetQuery, { includeToken: true });
484
- }
485
-
486
- app.post(routePath(BASE_PATH, '/api/share/qr-ticket'), express.json({ limit: '8kb' }), (req, res) => {
487
- try {
488
- const targetQuery = shareTargetQueryFromBody(req.body);
489
- const ticket = qrShareTickets.create(authEnabled ? tokenAuth.getToken() : '', { targetQuery });
490
- const shortPath = routePath(BASE_PATH, `/j/${ticket.code}`);
491
- const longPath = entryPathWithToken(ticket.targetQuery);
492
- res.json({
493
- code: ticket.code,
494
- expiresAt: ticket.expiresAt,
495
- ttlMs: SHARE_TICKET_TTL_MS,
496
- shortPath,
497
- shortUrl: absoluteClientUrl(req, shortPath),
498
- longUrl: absoluteClientUrl(req, longPath),
499
- tokenLabel: authEnabled ? tokenAuth.getToken() : '',
500
- });
501
- } catch (error) {
502
- res.status(500).json({ error: error.message || 'Failed to create share ticket' });
503
- }
504
- });
505
-
506
- app.delete(routePath(BASE_PATH, '/api/share/qr-ticket/:code'), (req, res) => {
507
- res.json({ revoked: qrShareTickets.revoke(req.params.code) });
508
- });
509
-
510
- // Terminal assets remain available to the standalone CRT skin when React is served from dist.
511
- app.use(routePath(BASE_PATH, '/vendor'), express.static(path.join(frontendDir, 'vendor')));
512
- app.get(routePath(BASE_PATH, '/vendor/xterm/xterm.js'), (_req, res) => {
513
- res.sendFile(xtermBrowserEntryPath);
514
- });
515
- app.get(routePath(BASE_PATH, '/vendor/xterm/addon-fit.js'), (_req, res) => {
516
- res.sendFile(xtermFitEntryPath);
517
- });
518
- app.get(routePath(BASE_PATH, '/vendor/xterm/addon-webgl.js'), (_req, res) => {
519
- res.sendFile(xtermWebglEntryPath);
520
- });
521
- app.get(routePath(BASE_PATH, '/vendor/xterm/xterm.css'), (_req, res) => {
522
- res.sendFile(xtermCssPath);
523
- });
524
- app.use(routePath(BASE_PATH, '/vendor/material-icons'), express.static(materialIconDir));
525
- app.get(routePath(BASE_PATH, '/vendor/material-icons/:iconId.svg'), (req, res) => {
526
- const fallbackIcon = String(req.params.iconId || '').startsWith('folder-') ? 'folder.svg' : 'file.svg';
527
- res.sendFile(path.join(materialIconDir, fallbackIcon));
528
- });
529
- if (BASE_PATH) {
530
- app.use('/assets', express.static(path.join(staticAppDir, 'assets'), { index: false }));
531
- app.use('/farming-2', express.static(path.join(staticAppDir, 'farming-2'), { index: false }));
532
- }
533
- const crtEntryPath = routePath(BASE_PATH, '/crt');
534
- app.get(crtEntryPath, (req, res) => {
535
- if (req.path.endsWith('/')) {
536
- res.setHeader('Cache-Control', 'no-cache');
537
- res.sendFile(path.join(crtFrontendDir, 'index.html'));
538
- return;
539
- }
540
- const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
541
- res.redirect(308, `${crtEntryPath}/${requestUrl.search}`);
542
- });
543
- app.use(`${crtEntryPath}/shared`, express.static(frontendDir, { index: false }));
544
- app.get(`${crtEntryPath}/crt-markdown-renderer.js`, (_req, res) => {
545
- res.sendFile(path.join(distDir, 'crt-markdown-renderer.js'));
546
- });
547
- app.get(`${crtEntryPath}/crt-markdown-renderer.css`, (_req, res) => {
548
- res.sendFile(path.join(distDir, 'crt-markdown-renderer.css'));
549
- });
550
- app.use(`${crtEntryPath}/crt-markdown-assets`, express.static(path.join(distDir, 'crt-markdown-assets'), { index: false }));
551
- app.get(`${crtEntryPath}/crt-mermaid-renderer.js`, (_req, res) => {
552
- res.sendFile(path.join(distDir, 'crt-mermaid-renderer.js'));
553
- });
554
- app.use(`${crtEntryPath}/`, express.static(crtFrontendDir, { index: false }));
555
- app.use(BASE_PATH || '/', express.static(staticAppDir, { index: false }));
556
-
557
- app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService, {
558
- rootRegistry: workspaceRootRegistry,
559
- }));
560
- app.use(routePath(BASE_PATH, '/api/browsers'), createBrowserRouter(browserResourceManager, workspaceRootRegistry));
561
-
562
- app.use(routePath(BASE_PATH, '/api/review-sessions'), createReviewSessionRouter(reviewSessionService));
563
- app.use(routePath(BASE_PATH, '/api/reviews'), createReviewDiffRouter(reviewDiffService, reviewSessionService));
564
- app.use(routePath(BASE_PATH, '/api/reviews'), createReviewStateRouter(reviewStateStore));
565
-
566
- if (process.env.NODE_ENV === 'test' && process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
567
- app.post(routePath(BASE_PATH, '/api/control/e2e/close-websockets'), express.json(), (_req, res) => {
568
- const clients = [...wss.clients].filter(client => client.readyState === WebSocket.OPEN);
569
- res.json({ closing: clients.length, code: 1013 });
570
- setImmediate(() => {
571
- clients.forEach(client => client.close(1013, 'terminal stream backpressure test'));
572
- });
573
- });
574
- }
575
-
576
- app.use(routePath(BASE_PATH, '/api/control'), createControlRouter(agentManager, {
577
- notifyUpdate: broadcastState,
578
- allowConcurrentTestControl: process.env.NODE_ENV === 'test'
579
- && process.env.FARMING_E2E_FAKE_EXECUTABLES === '1',
580
- }));
581
-
582
- app.get([
583
- BASE_PATH || '/',
584
- `${BASE_PATH || ''}/`,
585
- routePath(BASE_PATH, '/code'),
586
- routePath(BASE_PATH, '/code/'),
587
- routePath(BASE_PATH, '/error-preview'),
588
- routePath(BASE_PATH, '/review'),
589
- ].filter(Boolean), (req, res) => {
590
- const indexPath = path.join(staticAppDir, 'index.html');
591
- fs.readFile(indexPath, 'utf8', (error, html) => {
592
- if (error) {
593
- res.status(500).send('Farming frontend is not built');
594
- return;
595
- }
596
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
597
- res.setHeader('Cache-Control', 'no-cache');
598
- const rewrittenHtml = applyIndexHtmlAppearance(
599
- rewriteIndexHtmlForBasePath(html, BASE_PATH),
600
- configManager.getSettings().appearance
601
- );
602
- const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
603
- const requestToken = requestUrl.searchParams.has('token') ? tokenAuth.extractToken(req) : '';
604
- const htmlWithAssetToken = authEnabled && requestToken && tokenAuth.verify(requestToken)
605
- ? appendIndexHtmlAssetToken(rewrittenHtml, requestToken)
606
- : rewrittenHtml;
607
- res.send(htmlWithAssetToken);
608
- });
609
- });
610
-
611
- app.get(routePath(BASE_PATH, '/api/executables'), (req, res) => {
612
- const availableAgents = getAvailableAgentsForRequest();
613
- res.setHeader('Cache-Control', 'no-store');
614
- res.json({
615
- agents: availableAgents,
616
- total: availableAgents.length
617
- });
618
- });
619
-
620
- app.get(routePath(BASE_PATH, '/api/workspaces/complete'), async (req, res) => {
621
- try {
622
- const partialPath = typeof req.query.path === 'string' ? req.query.path : '';
623
- const requestedLimit = Number(req.query.limit);
624
- const suggestions = await listWorkspacePathCompletions(partialPath, requestedLimit);
625
- res.json({ suggestions });
626
- } catch (error) {
627
- res.status(200).json({
628
- suggestions: [],
629
- error: error.message || 'Failed to read directory',
630
- });
631
- }
632
- });
633
-
634
- app.use(routePath(BASE_PATH, '/api/workspaces'), createWorkspaceDirectoryRouter());
635
-
636
- app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
637
- res.json({ skills: getMainAgentSkillsCatalog() });
638
- });
639
-
640
- app.get(routePath(BASE_PATH, '/api/agent-extensions'), (_req, res) => {
641
- const agents = getAvailableAgentsForRequest()
642
- .filter(agent => agent.category === 'coding')
643
- .map(agent => {
644
- const provider = String(agent.name || agent.command || '').trim().toLowerCase();
645
- const configuredHomes = configManager.getAgentHomes(provider);
646
- const homes = configuredHomes.length > 0
647
- ? configuredHomes
648
- : [{ id: 'default', path: '' }];
649
- return {
650
- id: provider,
651
- name: agent.name,
652
- description: agent.description || '',
653
- discoverySupported: provider === 'codex' || provider === 'claude',
654
- homes: homes.map(home => ({
655
- id: home.id,
656
- extensions: discoverAgentExtensions({
657
- provider,
658
- providerHomePath: home.path,
659
- }).map(extension => ({
660
- id: extension.command,
661
- command: extension.command,
662
- name: extension.label,
663
- description: extension.description,
664
- kind: extension.source === 'custom' ? 'command' : extension.source || 'command',
665
- scope: extension.scope || '',
666
- })),
667
- })),
668
- };
669
- });
670
- res.setHeader('Cache-Control', 'no-store');
671
- res.json({ agents });
672
- });
673
-
674
- app.get(routePath(BASE_PATH, '/api/slash-commands'), (req, res) => {
675
- const provider = typeof req.query.provider === 'string' ? req.query.provider : '';
676
- const workspace = typeof req.query.workspace === 'string' ? req.query.workspace : '';
677
- res.json({ commands: discoverSlashCommands({ provider, workspace }) });
678
- });
679
-
680
- const IMAGE_ATTACHMENT_EXTENSIONS = {
681
- 'image/png': 'png',
682
- 'image/jpeg': 'jpg',
683
- 'image/gif': 'gif',
684
- 'image/webp': 'webp',
685
- };
686
- const AUDIO_ATTACHMENT_EXTENSIONS = {
687
- 'audio/aac': 'aac',
688
- 'audio/flac': 'flac',
689
- 'audio/mp4': 'm4a',
690
- 'audio/mpeg': 'mp3',
691
- 'audio/ogg': 'ogg',
692
- 'audio/wav': 'wav',
693
- 'audio/wave': 'wav',
694
- 'audio/webm': 'webm',
695
- 'audio/x-wav': 'wav',
696
- };
697
- const IMAGE_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
698
- const IMAGE_ATTACHMENT_GC_INTERVAL_MS = 60 * 60 * 1000;
699
- const IMAGE_ATTACHMENT_FILENAME_RE = /^pasted-image-\d+-[a-f0-9]{8}\.(?:png|jpg|gif|webp)$/;
700
- const AUDIO_ATTACHMENT_FILENAME_RE = /^pasted-audio-\d+-[a-f0-9]{8}\.(?:aac|flac|m4a|mp3|ogg|wav|webm)$/;
701
- let lastImageAttachmentGcAt = 0;
702
-
703
- function imageAttachmentExtension(contentType) {
704
- const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
705
- return IMAGE_ATTACHMENT_EXTENSIONS[normalized] || '';
706
- }
707
-
708
- function audioAttachmentExtension(contentType) {
709
- const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
710
- return AUDIO_ATTACHMENT_EXTENSIONS[normalized] || '';
711
- }
712
-
713
- function imageAttachmentsDir() {
714
- return path.join(configManager.farmingDir, 'attachments');
715
- }
716
-
717
- async function cleanupExpiredImageAttachments(options = {}) {
718
- const now = Date.now();
719
- if (!options.force && now - lastImageAttachmentGcAt < IMAGE_ATTACHMENT_GC_INTERVAL_MS) return;
720
- lastImageAttachmentGcAt = now;
721
-
722
- const attachmentsDir = imageAttachmentsDir();
723
- let entries = [];
724
- try {
725
- entries = await fs.promises.readdir(attachmentsDir, { withFileTypes: true });
726
- } catch (error) {
727
- if (error && error.code !== 'ENOENT') {
728
- console.warn('Failed to scan image attachments:', error.message || error);
729
- }
730
- return;
731
- }
732
-
733
- const cutoff = now - IMAGE_ATTACHMENT_RETENTION_MS;
734
- await Promise.all(entries.map(async (entry) => {
735
- if (!entry.isFile() || (!IMAGE_ATTACHMENT_FILENAME_RE.test(entry.name) && !AUDIO_ATTACHMENT_FILENAME_RE.test(entry.name))) return;
736
-
737
- const filePath = path.join(attachmentsDir, entry.name);
738
- try {
739
- const stat = await fs.promises.stat(filePath);
740
- if (stat.mtimeMs < cutoff) {
741
- await fs.promises.unlink(filePath);
742
- }
743
- } catch (error) {
744
- if (!error || error.code !== 'ENOENT') {
745
- console.warn('Failed to remove expired image attachment:', error && (error.message || error));
746
- }
747
- }
748
- }));
749
- }
750
-
751
- void cleanupExpiredImageAttachments({ force: true });
752
-
753
- app.post(
754
- routePath(BASE_PATH, '/api/attachments/image'),
755
- express.raw({ type: 'image/*', limit: '12mb' }),
756
- (req, res) => {
757
- const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
758
- const extension = imageAttachmentExtension(contentType);
759
- if (!extension) {
760
- res.status(415).json({ error: 'unsupported image type' });
761
- return;
762
- }
763
-
764
- if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
765
- res.status(400).json({ error: 'empty image attachment' });
766
- return;
767
- }
768
-
769
- const attachmentsDir = imageAttachmentsDir();
770
- fs.mkdirSync(attachmentsDir, { recursive: true });
771
- const filename = `pasted-image-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.${extension}`;
772
- const filePath = path.join(attachmentsDir, filename);
773
- fs.writeFileSync(filePath, req.body);
774
- void cleanupExpiredImageAttachments();
775
-
776
- res.status(201).json({
777
- path: filePath,
778
- name: filename,
779
- type: contentType,
780
- size: req.body.length,
781
- });
782
- }
783
- );
784
-
785
- app.post(
786
- routePath(BASE_PATH, '/api/attachments/audio'),
787
- express.raw({ type: 'audio/*', limit: '25mb' }),
788
- (req, res) => {
789
- const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
790
- const extension = audioAttachmentExtension(contentType);
791
- if (!extension) {
792
- res.status(415).json({ error: 'unsupported audio type' });
793
- return;
794
- }
795
- if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
796
- res.status(400).json({ error: 'empty audio attachment' });
797
- return;
798
- }
799
- const attachmentsDir = imageAttachmentsDir();
800
- fs.mkdirSync(attachmentsDir, { recursive: true });
801
- const filename = `pasted-audio-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.${extension}`;
802
- const filePath = path.join(attachmentsDir, filename);
803
- fs.writeFileSync(filePath, req.body);
804
- void cleanupExpiredImageAttachments();
805
- res.status(201).json({ path: filePath, name: filename, type: contentType, size: req.body.length });
806
- }
807
- );
808
-
809
- app.get(routePath(BASE_PATH, '/api/codex/models'), async (req, res) => {
810
- try {
811
- const catalog = await codexModelOptionsCache.get('catalog');
812
- res.json(catalog);
813
- } catch (error) {
814
- const timedOut = error && error.code === 'CODEX_MODELS_TIMEOUT';
815
- res.status(timedOut ? 504 : 502).json({
816
- error: error && error.message ? error.message : 'Failed to load Codex model catalog',
817
- code: error && error.code ? error.code : 'CODEX_MODELS_FAILED',
818
- });
819
- }
820
- });
821
-
822
- app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
823
- res.json({ settings: readClaudeSettingsSummary() });
824
- });
825
-
826
- app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
827
- try {
828
- const fresh = req.query.fresh === '1';
829
- const live = req.query.live === '1';
830
- if (fresh) usageMonitor.invalidateDailyCache();
831
- const usage = await usageSummaryCache.get(
832
- 'summary',
833
- fresh ? { force: true } : live ? { maxAgeMs: 15_000 } : {},
834
- );
835
- res.json({ usage });
836
- } catch (error) {
837
- res.status(500).json({ error: error.message || 'Failed to read usage information' });
838
- }
839
- });
840
-
841
- app.get(routePath(BASE_PATH, '/api/usage/day'), async (req, res) => {
842
- try {
843
- const date = String(req.query.date || '').trim();
844
- const detail = await usageMonitor.getUsageDay(date, {
845
- fresh: req.query.fresh === '1',
846
- live: req.query.live === '1',
847
- });
848
- res.json({ detail });
849
- } catch (error) {
850
- const invalidDate = error instanceof RangeError;
851
- res.status(invalidDate ? 400 : 500).json({
852
- error: invalidDate
853
- ? error.message
854
- : error.message || 'Failed to read usage day information',
855
- });
856
- }
857
- });
858
-
859
- app.post(routePath(BASE_PATH, '/api/codex/context-windows'), express.json(), async (req, res) => {
860
- try {
861
- const requestedIds = Array.isArray(req.body?.agentIds)
862
- ? req.body.agentIds
863
- .map(value => String(value || '').trim())
864
- .filter(Boolean)
865
- .slice(0, 20)
866
- : [];
867
- const requestedIdSet = new Set(requestedIds);
868
- const agents = agentManager.getState().agents.filter(agent => requestedIdSet.has(agent.id));
869
- const contextWindows = await codexContextWindowReader.readForAgents(agents);
870
- res.json({ contextWindows });
871
- } catch (error) {
872
- res.status(500).json({ error: error.message || 'Failed to read Codex context windows' });
873
- }
874
- });
875
-
876
- app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
877
- try {
878
- const update = await updateService.check({ force: req.query.force === '1' });
879
- res.json({ update });
880
- } catch (error) {
881
- res.status(502).json({ error: error.message || 'Failed to check for updates' });
882
- }
883
- });
884
-
885
- app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req, res) => {
886
- try {
887
- const state = await updateService.startInstall({
888
- assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
889
- });
890
- res.status(202).json({ update: { state } });
891
- } catch (error) {
892
- res.status(500).json({ error: error.message || 'Failed to start update' });
893
- }
894
- });
895
-
896
- app.post(routePath(BASE_PATH, '/api/update/restart'), express.json(), async (req, res) => {
897
- try {
898
- const state = await updateService.applyPreparedUpdate();
899
- res.status(202).json({ update: { state } });
900
- } catch (error) {
901
- res.status(500).json({ error: error.message || 'Failed to restart for update' });
902
- }
903
- });
904
-
905
- function warmCodexExecutableVersionCache() {
906
- const startedAt = Date.now();
907
- try {
908
- const result = resolveCompatibleCodexExecutable('');
909
- if (result.path) {
910
- console.log(`Codex executable ready: ${result.version || 'unknown version'} (${Date.now() - startedAt}ms)`);
911
- }
912
- } catch (error) {
913
- console.warn('Failed to warm Codex executable version cache:', error.message || error);
914
- }
915
- }
916
-
917
- app.get(routePath(BASE_PATH, '/api/codex/sessions'), async (req, res) => {
918
- const requestedLimit = Number(req.query.limit);
919
- const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(1000, requestedLimit)) : 40;
920
- const requestedScanLimit = Number(req.query.scanLimit);
921
- const scanLimit = Number.isFinite(requestedScanLimit) ? Math.max(limit, Math.min(5000, requestedScanLimit)) : undefined;
922
- const sessions = await listCodexSessions({ limit, scanLimit });
923
- res.json({ sessions });
924
- });
925
-
926
- app.get(routePath(BASE_PATH, '/api/agent-sessions'), async (req, res) => {
927
- try {
928
- const requestedLimit = Number(req.query.limit);
929
- const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(1000, requestedLimit)) : 60;
930
- const cursor = typeof req.query.cursor === 'string' ? req.query.cursor : '';
931
- const cacheOptions = req.query.force === '1'
932
- ? { force: true }
933
- : (req.query.fresh === '1' ? { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS } : {});
934
- const sessions = await agentSessionsCache.get(
935
- JSON.stringify(configManager.getSettings().agentHomes || {}),
936
- cacheOptions
937
- );
938
- const page = paginateAgentSessions(sessions, { limit: Math.max(1, limit), cursor });
939
- if (page.invalidCursor) {
940
- res.status(400).json({ error: 'Invalid Agent session cursor' });
941
- return;
942
- }
943
- const displayStateByKey = new Map(configManager.listAgentSessionRecords()
944
- .filter(record => record && record.providerSessionKey)
945
- .map(record => [record.providerSessionKey, record]));
946
- res.json({
947
- sessions: page.sessions.map(session => {
948
- const key = mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId);
949
- const displayState = displayStateByKey.get(key);
950
- return typeof displayState?.displayPinned === 'boolean'
951
- ? { ...session, pinned: displayState.displayPinned }
952
- : session;
953
- }),
954
- nextCursor: page.nextCursor,
955
- hasMore: page.hasMore,
956
- total: sessions.length,
957
- });
958
- } catch (error) {
959
- console.error('Failed to read agent sessions:', error);
960
- res.status(500).json({ error: error.message || 'Failed to read agent sessions' });
961
- }
962
- });
963
-
964
- app.get(routePath(BASE_PATH, '/api/agent-sessions/search'), async (req, res) => {
965
- try {
966
- const query = typeof req.query.q === 'string' ? req.query.q : '';
967
- const requestedLimit = Number(req.query.limit);
968
- const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(1000, requestedLimit)) : 100;
969
- const settings = configManager.getSettings();
970
- const cacheOptions = req.query.force === '1'
971
- ? { force: true }
972
- : (req.query.fresh === '1' ? { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS } : {});
973
- const sessions = await withSearchTimeout(
974
- agentSessionsCache.get(
975
- JSON.stringify(settings.agentHomes || {}),
976
- cacheOptions
977
- ),
978
- settings.searchTimeoutMs
979
- );
980
- const result = searchAgentSessions(sessions, query, {
981
- limit,
982
- projectNames: settings.projectNames,
983
- });
984
- const displayStateByKey = new Map(configManager.listAgentSessionRecords()
985
- .filter(record => record && record.providerSessionKey)
986
- .map(record => [record.providerSessionKey, record]));
987
- res.json({
988
- ...result,
989
- sessions: result.sessions.map(session => {
990
- const key = mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId);
991
- const displayState = displayStateByKey.get(key);
992
- return typeof displayState?.displayPinned === 'boolean'
993
- ? { ...session, pinned: displayState.displayPinned }
994
- : session;
995
- }),
996
- });
997
- } catch (error) {
998
- if (error?.code === 'ETIMEDOUT') {
999
- res.status(504).json({ error: error.message });
1000
- return;
1001
- }
1002
- console.error('Failed to search agent sessions:', error);
1003
- res.status(500).json({ error: error.message || 'Failed to search Agent sessions' });
1004
- }
1005
- });
1006
-
1007
- app.patch(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId'), express.json(), (req, res) => {
1008
- const provider = normalizeProvider(req.params.provider);
1009
- const sessionId = String(req.params.sessionId || '').trim();
1010
- const providerHomeId = String(req.body?.providerHomeId || 'default').trim() || 'default';
1011
- if (!provider || !isSafeSessionId(sessionId) || !/^[A-Za-z0-9._-]+$/.test(providerHomeId)) {
1012
- res.status(400).json({ error: 'Invalid Agent session' });
1013
- return;
1014
- }
1015
- if (typeof req.body?.pinned !== 'boolean') {
1016
- res.status(400).json({ error: 'Pinned state is required' });
1017
- return;
1018
- }
1019
- const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
1020
- configManager.setProviderSessionDisplayState(sessionKey, { pinned: req.body.pinned });
1021
- res.json({ sessionKey, pinned: req.body.pinned });
1022
- });
1023
-
1024
- app.post(routePath(BASE_PATH, '/api/main-page-agent-sessions'), express.json(), (req, res) => {
1025
- const operation = req.body?.operation;
1026
- const requestedKeys = Array.isArray(req.body?.sessionKeys) ? req.body.sessionKeys : [];
1027
- const sessionKeys = [...new Set(requestedKeys.map(key => String(key || '').trim()).filter(Boolean))];
1028
- if (
1029
- !['add', 'remove'].includes(operation)
1030
- || sessionKeys.length === 0
1031
- || sessionKeys.length > 50
1032
- || sessionKeys.some(key => !mainPageAgentSessionFromKey(key))
1033
- ) {
1034
- res.status(400).json({ error: 'A valid main-page Agent session mutation is required' });
1035
- return;
1036
- }
1037
-
1038
- if (operation === 'add') {
1039
- [...sessionKeys].reverse().forEach(sessionKey => {
1040
- const session = mainPageAgentSessionFromKey(sessionKey);
1041
- configManager.rememberMainPageSessionKey(sessionKey, {
1042
- provider: session.provider,
1043
- providerSessionId: session.sessionId,
1044
- providerSessionKey: sessionKey,
1045
- providerHomeId: session.providerHomeId || 'default',
1046
- });
1047
- });
1048
- } else {
1049
- configManager.removeMainPageSessionKeys(sessionKeys);
1050
- }
1051
-
1052
- const mainPageSessionKeys = configManager.getMainPageSessionKeys();
1053
- agentSessionsCache.invalidate();
1054
- res.json({ success: true, mainPageSessionKeys });
1055
- scheduleBroadcastState();
1056
- });
1057
-
1058
- app.get(routePath(BASE_PATH, '/api/themes'), (req, res) => {
1059
- const currentTheme = configManager.getSettings().theme || 'terminal';
1060
- res.json({
1061
- themes: themeManager.getAllThemes(),
1062
- current: currentTheme
1063
- });
1064
- });
1065
-
1066
- app.get(routePath(BASE_PATH, '/api/settings'), (req, res) => {
1067
- res.json({
1068
- settings: configManager.getSettings()
1069
- });
1070
- });
1071
-
1072
- app.get(routePath(BASE_PATH, '/api/workspaces/discovered'), (req, res) => {
1073
- const requestedLimit = Number(req.query.limit);
1074
- const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.min(20, requestedLimit)) : 12;
1075
- const agent = typeof req.query.agent === 'string' ? req.query.agent : '';
1076
- const cacheToken = JSON.stringify({ limit, agent });
1077
- workspaceDiscoveryCache.get(cacheToken)
1078
- .then(workspaces => {
1079
- res.json({ workspaces });
1080
- })
1081
- .catch(error => {
1082
- console.error('Failed to discover workspaces:', error);
1083
- res.status(500).json({ error: error.message || 'Failed to discover workspaces' });
1084
- });
1085
- });
1086
-
1087
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-text'), async (req, res) => {
1088
- const text = await agentManager.getAgentSessionText(req.params.agentId);
1089
- if (text === null) {
1090
- res.status(404).json({ error: 'Agent not found' });
1091
- return;
1092
- }
1093
-
1094
- res.type('text/plain');
1095
- res.send(text);
1096
- });
1097
-
1098
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-view'), async (req, res) => {
1099
- const sessionView = await agentManager.getAgentSessionView(req.params.agentId);
1100
- if (!sessionView) {
1101
- res.status(404).json({ error: 'Agent not found' });
1102
- return;
1103
- }
1104
-
1105
- res.json({ session: sessionView });
1106
- });
1107
-
1108
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (req, res) => {
1109
- // Legacy JSONL transcript reader. ACP Chat uses the dedicated ACP endpoint;
1110
- // Terminal Agents stay in their terminal UI.
1111
- const providerSession = agentManager.getAgentProviderSession(req.params.agentId);
1112
- if (!providerSession) {
1113
- res.status(404).json({ error: 'Agent not found' });
1114
- return;
1115
- }
1116
- if (
1117
- providerSession.provider !== 'codex'
1118
- || providerSession.temporary
1119
- || !providerSession.sessionId
1120
- || providerSession.sessionId.startsWith('tmp_uuid')
1121
- ) {
1122
- res.json({
1123
- transcript: {
1124
- available: false,
1125
- reason: 'not-codex-provider-session',
1126
- sessionId: providerSession.sessionId || '',
1127
- turns: [],
1128
- },
1129
- });
1130
- return;
1131
- }
1132
-
1133
- try {
1134
- const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
1135
- const maxTurns = Number.isFinite(requestedMaxTurns)
1136
- ? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
1137
- : DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
1138
- const transcript = await readCodexTranscript(providerSession.sessionId, {
1139
- maxTurns,
1140
- codexHome: providerSession.providerHomePath || '',
1141
- });
1142
- res.json({ transcript });
1143
- } catch (error) {
1144
- console.error('Failed to read Codex transcript:', error);
1145
- res.status(500).json({ error: error.message || 'Failed to read Codex transcript' });
1146
- }
1147
- });
1148
-
1149
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/json-cli-transcript'), async (req, res) => {
1150
- try {
1151
- const transcript = agentManager.getJsonCliTranscript(req.params.agentId, {
1152
- maxTurns: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS,
1153
- });
1154
- res.json({ transcript });
1155
- } catch (error) {
1156
- const message = error && error.message ? error.message : 'Failed to read JSON CLI transcript';
1157
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1158
- }
1159
- });
1160
-
1161
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), async (req, res) => {
1162
- try {
1163
- res.json({
1164
- session: agentManager.getAcpSession(req.params.agentId, {
1165
- includeUpdates: req.query.includeUpdates === '1',
1166
- includeEntries: req.query.includeEntries !== '0',
1167
- }),
1168
- });
1169
- } catch (error) {
1170
- const message = error && error.message ? error.message : 'Failed to read ACP session';
1171
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1172
- }
1173
- });
1174
-
1175
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req, res) => {
1176
- try {
1177
- const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
1178
- const maxTurns = Number.isFinite(requestedMaxTurns)
1179
- ? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
1180
- : DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
1181
- const requestedRevision = Number.parseInt(String(req.query.sinceRevision || ''), 10);
1182
- const externalMedia = req.query.media === 'external-v1';
1183
- res.json({ transcript: agentManager.getAcpTranscript(req.params.agentId, {
1184
- maxTurns,
1185
- ...(externalMedia
1186
- ? {
1187
- mediaPathPrefix: routePath(
1188
- BASE_PATH,
1189
- `/api/agents/${encodeURIComponent(req.params.agentId)}/acp-media`
1190
- ),
1191
- }
1192
- : {}),
1193
- ...(Number.isFinite(requestedRevision) && requestedRevision >= 0
1194
- ? { sinceRevision: requestedRevision }
1195
- : {}),
1196
- }) });
1197
- } catch (error) {
1198
- const message = error && error.message ? error.message : 'Failed to read ACP transcript';
1199
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1200
- }
1201
- });
1202
-
1203
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-media/:entryId/:mediaId'), (req, res) => {
1204
- try {
1205
- const media = agentManager.getAcpTranscriptMedia(
1206
- req.params.agentId,
1207
- req.params.entryId,
1208
- req.params.mediaId
1209
- );
1210
- const decoded = decodeAcpTranscriptMedia(media);
1211
- if (!decoded) {
1212
- res.status(415).json({ error: 'unsupported ACP transcript media' });
1213
- return;
1214
- }
1215
- res.setHeader('Cache-Control', 'private, max-age=31536000, immutable');
1216
- res.setHeader('X-Content-Type-Options', 'nosniff');
1217
- res.type(decoded.mimeType).send(decoded.content);
1218
- } catch (error) {
1219
- const message = error && error.message ? error.message : 'Failed to read ACP transcript media';
1220
- const status = message === 'Agent not found'
1221
- || message === 'ACP transcript entry not found'
1222
- || message === 'ACP transcript media not found'
1223
- ? 404
1224
- : 409;
1225
- res.status(status).json({ error: message });
1226
- }
1227
- });
1228
-
1229
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-tool-details/:toolCallId'), async (req, res) => {
1230
- try {
1231
- res.json(agentManager.getAcpToolDetail(req.params.agentId, req.params.toolCallId));
1232
- } catch (error) {
1233
- const message = error && error.message ? error.message : 'Failed to read ACP tool details';
1234
- const status = message === 'Agent not found' || message === 'ACP tool call not found' ? 404 : 409;
1235
- res.status(status).json({ error: message });
1236
- }
1237
- });
1238
-
1239
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/kill'), (req, res) => {
1240
- try {
1241
- res.json(agentManager.killAcpTerminal(req.params.agentId, req.params.terminalId));
1242
- } catch (error) {
1243
- const message = error && error.message ? error.message : 'Failed to stop ACP terminal';
1244
- const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
1245
- res.status(status).json({ error: message });
1246
- }
1247
- });
1248
-
1249
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/input'), express.json({ limit: '96kb' }), (req, res) => {
1250
- try {
1251
- res.json(agentManager.inputAcpTerminal(req.params.agentId, req.params.terminalId, req.body?.input));
1252
- } catch (error) {
1253
- const message = error && error.message ? error.message : 'Failed to write ACP terminal input';
1254
- const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
1255
- res.status(status).json({ error: message });
1256
- }
1257
- });
1258
-
1259
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-terminals/:terminalId/resize'), express.json(), (req, res) => {
1260
- try {
1261
- res.json(agentManager.resizeAcpTerminal(
1262
- req.params.agentId,
1263
- req.params.terminalId,
1264
- req.body?.cols,
1265
- req.body?.rows,
1266
- ));
1267
- } catch (error) {
1268
- const message = error && error.message ? error.message : 'Failed to resize ACP terminal';
1269
- const status = message === 'Agent not found' || message === 'Unknown ACP terminal' ? 404 : 409;
1270
- res.status(status).json({ error: message });
1271
- }
1272
- });
1273
-
1274
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-subagents/:sessionId/cancel'), async (req, res) => {
1275
- try {
1276
- res.json(await agentManager.cancelAcpSubagent(req.params.agentId, req.params.sessionId));
1277
- } catch (error) {
1278
- const message = error && error.message ? error.message : 'Failed to stop ACP subagent';
1279
- const status = message === 'Agent not found' || message === 'ACP subagent session not found' ? 404 : 409;
1280
- res.status(status).json({ error: message });
1281
- }
1282
- });
1283
-
1284
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-patches/:toolCallId/decision'), express.json({ limit: '16kb' }), async (req, res) => {
1285
- try {
1286
- res.json(await agentManager.decideAcpPatch(
1287
- req.params.agentId,
1288
- req.params.toolCallId,
1289
- req.body?.path,
1290
- req.body?.decision,
1291
- ));
1292
- } catch (error) {
1293
- const message = error && error.message ? error.message : 'Failed to decide ACP patch';
1294
- const status = Number(error?.statusCode) || (message === 'Agent not found' || message === 'ACP tool call not found' ? 404 : 409);
1295
- res.status(status).json({ error: message });
1296
- }
1297
- });
1298
-
1299
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-sessions'), async (req, res) => {
1300
- try {
1301
- const result = await agentManager.listAcpSessions(req.params.agentId, {
1302
- cwd: typeof req.query.cwd === 'string' ? req.query.cwd : '',
1303
- cursor: typeof req.query.cursor === 'string' ? req.query.cursor : '',
1304
- });
1305
- res.json(result);
1306
- } catch (error) {
1307
- const message = error && error.message ? error.message : 'Failed to list ACP sessions';
1308
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1309
- }
1310
- });
1311
-
1312
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-permission'), express.json(), (req, res) => {
1313
- try {
1314
- const result = agentManager.respondToAcpPermission(
1315
- req.params.agentId,
1316
- req.body?.requestId,
1317
- req.body?.optionId,
1318
- req.body?.cancelled === true
1319
- );
1320
- res.json(result);
1321
- } catch (error) {
1322
- const message = error && error.message ? error.message : 'Failed to respond to ACP permission';
1323
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1324
- }
1325
- });
1326
-
1327
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-elicitation'), express.json(), (req, res) => {
1328
- try {
1329
- const result = agentManager.respondToAcpElicitation(
1330
- req.params.agentId,
1331
- req.body?.requestId,
1332
- req.body?.action,
1333
- req.body?.content
1334
- );
1335
- res.json(result);
1336
- } catch (error) {
1337
- const message = error && error.message ? error.message : 'Failed to respond to ACP input request';
1338
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1339
- }
1340
- });
1341
-
1342
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/authenticate'), express.json(), async (req, res) => {
1343
- try {
1344
- res.json(await agentManager.authenticateAcpAgent(req.params.agentId, req.body?.methodId));
1345
- } catch (error) {
1346
- const message = error && error.message ? error.message : 'Failed to authenticate ACP Agent';
1347
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1348
- }
1349
- });
1350
-
1351
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/logout'), async (req, res) => {
1352
- try {
1353
- res.json(await agentManager.logoutAcpAgent(req.params.agentId));
1354
- } catch (error) {
1355
- const message = error && error.message ? error.message : 'Failed to log out ACP Agent';
1356
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1357
- }
1358
- });
1359
-
1360
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/fork'), express.json(), async (req, res) => {
1361
- try {
1362
- res.json(await agentManager.forkAcpSession(req.params.agentId, req.body || {}));
1363
- } catch (error) {
1364
- const message = error && error.message ? error.message : 'Failed to fork ACP session';
1365
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1366
- }
1367
- });
1368
-
1369
- app.delete(routePath(BASE_PATH, '/api/agents/:agentId/acp-sessions/:sessionId'), async (req, res) => {
1370
- try {
1371
- res.json(await agentManager.deleteAcpSession(req.params.agentId, req.params.sessionId));
1372
- } catch (error) {
1373
- const message = error && error.message ? error.message : 'Failed to delete ACP session';
1374
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1375
- }
1376
- });
1377
-
1378
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/close'), async (req, res) => {
1379
- try {
1380
- res.json(await agentManager.closeAcpSession(req.params.agentId));
1381
- } catch (error) {
1382
- const message = error && error.message ? error.message : 'Failed to close ACP session';
1383
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1384
- }
1385
- });
1386
-
1387
- app.patch(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), express.json(), async (req, res) => {
1388
- try {
1389
- if (typeof req.body?.modeId === 'string') {
1390
- res.json(await agentManager.setAcpSessionMode(req.params.agentId, req.body.modeId));
1391
- return;
1392
- }
1393
- if (Array.isArray(req.body?.configOptions)) {
1394
- res.json(await agentManager.setAcpSessionConfigOptions(req.params.agentId, req.body.configOptions));
1395
- return;
1396
- }
1397
- if (typeof req.body?.configId === 'string' && Object.prototype.hasOwnProperty.call(req.body, 'value')) {
1398
- res.json(await agentManager.setAcpSessionConfigOption(
1399
- req.params.agentId,
1400
- req.body.configId,
1401
- req.body.value
1402
- ));
1403
- return;
1404
- }
1405
- res.status(400).json({ error: 'ACP modeId, configOptions, or configId/value is required' });
1406
- } catch (error) {
1407
- const message = error && error.message ? error.message : 'Failed to update ACP session';
1408
- res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1409
- }
1410
- });
1411
-
1412
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), express.json(), async (req, res) => {
1413
- const requestAbort = new globalThis.AbortController();
1414
- const abortRequest = () => requestAbort.abort(new Error('Terminal profile request was canceled'));
1415
- const abortClosedResponse = () => {
1416
- if (!res.writableEnded) abortRequest();
1417
- };
1418
- req.once('aborted', abortRequest);
1419
- res.once('close', abortClosedResponse);
1420
- try {
1421
- const profile = await agentManager.setCodexTerminalProfile(req.params.agentId, {
1422
- model: req.body?.model,
1423
- effort: req.body?.effort,
1424
- serviceTier: req.body?.serviceTier,
1425
- }, {
1426
- signal: requestAbort.signal,
1427
- timeoutMs: 44_000,
1428
- });
1429
- res.json({ profile });
1430
- } catch (error) {
1431
- const message = error && error.message ? error.message : 'Failed to update Codex Terminal profile';
1432
- const status = message === 'Agent not found'
1433
- ? 404
1434
- : /^A valid Codex /.test(message)
1435
- ? 400
1436
- : 409;
1437
- if (!res.headersSent && !res.destroyed) res.status(status).json({ error: message });
1438
- } finally {
1439
- req.off('aborted', abortRequest);
1440
- res.off('close', abortClosedResponse);
1441
- }
1442
- });
1443
-
1444
- app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
1445
- if (!await requireAgentRecoveryForHttp(res)) return;
1446
- const body = req.body || {};
1447
- const updates = {};
1448
- const providedPatchFields = [
1449
- 'customTitle',
1450
- 'task',
1451
- 'pinned',
1452
- 'unread',
1453
- 'archived',
1454
- 'readAttentionSeq',
1455
- 'readOutputEpoch',
1456
- 'readOutputSeq',
1457
- 'launchPermissionMode',
1458
- 'agentRuntimeMode',
1459
- ].filter(field => Object.prototype.hasOwnProperty.call(body, field));
1460
- const lifecyclePatchFields = providedPatchFields.filter(field => (
1461
- field === 'launchPermissionMode'
1462
- || field === 'agentRuntimeMode'
1463
- || (field === 'archived' && body.archived === true)
1464
- ));
1465
- if (lifecyclePatchFields.length > 0 && providedPatchFields.length > 1) {
1466
- res.status(400).json({
1467
- error: 'Archive, permission restart, and runtime switch must be requested separately from other Agent updates',
1468
- });
1469
- return;
1470
- }
1471
- const ordinaryPatchGroups = [
1472
- providedPatchFields.includes('customTitle') ? 'customTitle' : '',
1473
- providedPatchFields.includes('task') ? 'task' : '',
1474
- providedPatchFields.some(field => [
1475
- 'pinned',
1476
- 'unread',
1477
- 'archived',
1478
- 'readAttentionSeq',
1479
- 'readOutputEpoch',
1480
- 'readOutputSeq',
1481
- ].includes(field)) ? 'flags' : '',
1482
- ].filter(Boolean);
1483
- if (ordinaryPatchGroups.length > 1) {
1484
- res.status(400).json({
1485
- error: 'Agent title, task, and flags must be updated in separate requests',
1486
- });
1487
- return;
1488
- }
1489
-
1490
- await agentManager.whenAgentLifecycleIdle(req.params.agentId);
1491
-
1492
- if (typeof body.customTitle === 'string') {
1493
- const result = agentManager.renameAgent(req.params.agentId, body.customTitle);
1494
- if (result.error) {
1495
- const status = result.error === 'Agent not found'
1496
- ? 404
1497
- : (result.error.startsWith('Failed to ') ? 500 : 409);
1498
- res.status(status).json({ error: result.error });
1499
- return;
1500
- }
1501
- updates.customTitle = result.customTitle;
1502
- }
1503
-
1504
- if (typeof body.task === 'string') {
1505
- const result = agentManager.setAgentTask(req.params.agentId, body.task);
1506
- if (result.error) {
1507
- const status = result.error === 'Agent not found'
1508
- ? 404
1509
- : (result.error.startsWith('Failed to ') ? 500 : 409);
1510
- res.status(status).json({ error: result.error });
1511
- return;
1512
- }
1513
- updates.task = result.task;
1514
- }
1515
-
1516
- const flagPatch = {};
1517
- ['pinned', 'unread', 'archived'].forEach((flagName) => {
1518
- if (typeof body[flagName] === 'boolean') {
1519
- flagPatch[flagName] = body[flagName];
1520
- }
1521
- });
1522
- if (typeof body.readAttentionSeq === 'number' && Number.isFinite(body.readAttentionSeq)) {
1523
- flagPatch.readAttentionSeq = body.readAttentionSeq;
1524
- }
1525
- if (
1526
- typeof body.readOutputEpoch === 'string'
1527
- && body.readOutputEpoch
1528
- && typeof body.readOutputSeq === 'number'
1529
- && Number.isFinite(body.readOutputSeq)
1530
- ) {
1531
- flagPatch.readOutputEpoch = body.readOutputEpoch;
1532
- flagPatch.readOutputSeq = body.readOutputSeq;
1533
- }
1534
-
1535
- if (flagPatch.archived === true) {
1536
- const result = await agentManager.archiveAgent(req.params.agentId);
1537
- if (result.error) {
1538
- const status = result.stopped === true
1539
- ? 409
1540
- : (result.error === 'Agent not found' ? 404 : 400);
1541
- res.status(status).json(result);
1542
- return;
1543
- }
1544
- Object.assign(updates, result);
1545
- delete updates.agentId;
1546
- delete flagPatch.archived;
1547
- }
1548
-
1549
- let flagUpdateRequiresState = false;
1550
- if (Object.keys(flagPatch).length > 0) {
1551
- const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
1552
- if (result.error) {
1553
- const status = result.error === 'Agent not found'
1554
- ? 404
1555
- : (result.error.startsWith('Failed to ') ? 500 : 409);
1556
- res.status(status).json({ error: result.error });
1557
- return;
1558
- }
1559
- Object.assign(updates, result);
1560
- delete updates.agentId;
1561
- flagUpdateRequiresState = result.requiresState === true;
1562
- }
1563
-
1564
- if (typeof body.launchPermissionMode === 'string') {
1565
- const result = await agentManager.syncCodexTerminalPermissionMode(req.params.agentId, body.launchPermissionMode);
1566
- if (result.error) {
1567
- const status = result.error === 'Agent not found' ? 404 : 400;
1568
- res.status(status).json({ error: result.error });
1569
- return;
1570
- }
1571
- updates.launchPermissionMode = result.launchPermissionMode;
1572
- if (result.restarted === true) updates.restarted = true;
1573
- if (result.restartedAgentId) updates.restartedAgentId = result.restartedAgentId;
1574
- }
1575
-
1576
- if (typeof body.agentRuntimeMode === 'string') {
1577
- const result = await agentManager.restartAgentRuntimeMode(req.params.agentId, body.agentRuntimeMode);
1578
- if (result.error) {
1579
- const status = result.error === 'Agent not found' ? 404 : 400;
1580
- res.status(status).json({ error: result.error });
1581
- return;
1582
- }
1583
- updates.agentRuntimeMode = result.agentRuntimeMode;
1584
- if (result.restarted === true) updates.restarted = true;
1585
- if (result.restartedAgentId) updates.restartedAgentId = result.restartedAgentId;
1586
- if (result.switchFailed === true) updates.switchFailed = true;
1587
- if (result.warning) updates.warning = result.warning;
1588
- }
1589
-
1590
- if (Object.keys(updates).length === 0) {
1591
- res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, readOutputEpoch/readOutputSeq, launchPermissionMode, or agentRuntimeMode is required' });
1592
- return;
1593
- }
1594
-
1595
- if (flagUpdateRequiresState || typeof body.task === 'string' || typeof body.customTitle === 'string' || typeof body.launchPermissionMode === 'string' || typeof body.agentRuntimeMode === 'string') {
1596
- scheduleBroadcastState();
1597
- }
1598
- res.json({ agentId: req.params.agentId, ...updates });
1599
- });
1600
-
1601
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), async (req, res) => {
1602
- if (!await requireAgentRecoveryForHttp(res)) return;
1603
- await agentManager.whenAgentLifecycleIdle(req.params.agentId);
1604
- const result = agentManager.reorderAgent(req.params.agentId, {
1605
- beforeAgentId: req.body?.beforeAgentId,
1606
- afterAgentId: req.body?.afterAgentId,
1607
- });
1608
- if (result.error) {
1609
- const status = result.error === 'Agent not found'
1610
- ? 404
1611
- : (result.error.startsWith('Failed to ') ? 500 : 409);
1612
- res.status(status).json({ error: result.error });
1613
- return;
1614
- }
1615
- scheduleBroadcastState();
1616
- res.json(result);
1617
- });
1618
-
1619
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), async (req, res) => {
1620
- if (!await requireAgentRecoveryForHttp(res)) return;
1621
- const mode = req.body && typeof req.body.mode === 'string' ? req.body.mode : 'same-worktree';
1622
- const targetRuntime = req.body && typeof req.body.targetRuntime === 'string'
1623
- ? req.body.targetRuntime
1624
- : '';
1625
- if (targetRuntime && targetRuntime !== 'chat') {
1626
- res.status(400).json({ error: 'Unsupported Fork target runtime' });
1627
- return;
1628
- }
1629
- const requestId = typeof req.body?.requestId === 'string' ? req.body.requestId.trim() : '';
1630
- if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
1631
- res.status(400).json({ error: 'Fork requires a valid requestId' });
1632
- return;
1633
- }
1634
- const result = await agentManager.forkAgent(req.params.agentId, mode, {
1635
- requestId,
1636
- ...(targetRuntime ? { targetRuntime } : {}),
1637
- ...(Number.isSafeInteger(req.body?.expectedRevision)
1638
- ? { expectedRevision: req.body.expectedRevision }
1639
- : {}),
1640
- });
1641
- if (result.error) {
1642
- const status = result.error === 'Agent not found' ? 404 : 400;
1643
- res.status(status).json(result);
1644
- return;
1645
- }
1646
-
1647
- try {
1648
- const membership = configManager.mountProjectWorkspace(result.workspace);
1649
- result.projectWorkspaces = membership.projectWorkspaces;
1650
- result.pinnedProjectWorkspaces = membership.pinnedProjectWorkspaces;
1651
- } catch (error) {
1652
- broadcastState();
1653
- const mountError = error.message || 'Failed to create Project';
1654
- res.status(500).json({
1655
- ...result,
1656
- error: `${mountError}. Retry the same Fork request to reconcile Project membership.`,
1657
- retryable: true,
1658
- });
1659
- return;
1660
- }
1661
- broadcastState();
1662
- res.status(201).json(result);
1663
- });
1664
-
1665
- function resolveProjectActionRoot(rootId) {
1666
- const root = workspaceRootRegistry.resolve(rootId);
1667
- if (!root || root.kind === 'global') {
1668
- throw new WorkspaceFileError('project workspace is required', 400);
1669
- }
1670
- return root;
1671
- }
1672
-
1673
- app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (req, res) => {
1674
- try {
1675
- const root = resolveProjectActionRoot(req.body?.rootId);
1676
- const command = process.platform === 'darwin'
1677
- ? 'open'
1678
- : process.platform === 'win32'
1679
- ? 'explorer'
1680
- : 'xdg-open';
1681
- await execFileAsync(command, [root.canonicalPath], {
1682
- timeout: 15000,
1683
- maxBuffer: 1024 * 1024,
1684
- });
1685
- res.json({ revealed: true, workspace: root.canonicalPath });
1686
- } catch (error) {
1687
- const status = error instanceof WorkspaceFileError ? error.statusCode : 500;
1688
- res.status(status).json({ error: error.message || 'Failed to reveal project workspace' });
1689
- }
1690
- });
1691
-
1692
- app.post(routePath(BASE_PATH, '/api/projects/mount'), express.json(), async (req, res) => {
1693
- try {
1694
- const workspace = await canonicalProjectWorkspace(req.body?.workspace);
1695
- const membership = configManager.mountProjectWorkspace(workspace);
1696
- broadcastState();
1697
- res.json(membership);
1698
- } catch (error) {
1699
- res.status(400).json({ error: error.message || 'Failed to create Project' });
1700
- }
1701
- });
1702
-
1703
- app.post(routePath(BASE_PATH, '/api/projects/remove'), express.json(), (req, res) => {
1704
- try {
1705
- const membership = configManager.removeProjectWorkspace(req.body?.workspace);
1706
- broadcastState();
1707
- res.json(membership);
1708
- } catch (error) {
1709
- res.status(400).json({ error: error.message || 'Failed to remove Project' });
1710
- }
1711
- });
1712
-
1713
- app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) => {
1714
- try {
1715
- const membership = configManager.setProjectWorkspacePinned(
1716
- req.body?.workspace,
1717
- req.body?.pinned === true
1718
- );
1719
- broadcastState();
1720
- res.json(membership);
1721
- } catch (error) {
1722
- res.status(400).json({ error: error.message || 'Failed to update Project pin' });
1723
- }
1724
- });
1725
-
1726
- app.patch(routePath(BASE_PATH, '/api/projects/name'), express.json(), (req, res) => {
1727
- try {
1728
- const result = configManager.setProjectName(req.body?.workspace, req.body?.name);
1729
- scheduleBroadcastState();
1730
- res.json(result);
1731
- } catch (error) {
1732
- res.status(400).json({ error: error.message || 'Failed to rename Project' });
1733
- }
1734
- });
1735
-
1736
- app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
1737
- const requestId = typeof req.body?.requestId === 'string' ? req.body.requestId.trim() : '';
1738
- if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
1739
- res.status(400).json({ error: 'Project worktree creation requires a valid requestId' });
1740
- return;
1741
- }
1742
- try {
1743
- const root = resolveProjectActionRoot(req.body?.rootId);
1744
- const created = await agentManager.createPermanentWorktree(root.canonicalPath, { requestId });
1745
- broadcastState();
1746
- res.status(201).json({
1747
- ...created,
1748
- });
1749
- } catch (error) {
1750
- const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
1751
- const operation = configManager.getProjectOperation?.(requestId);
1752
- res.status(status).json({
1753
- error: error.message || 'Failed to create permanent worktree',
1754
- requestId,
1755
- ...(operation?.state === 'pending' ? { retryable: true } : {}),
1756
- ...(['unknown', 'blocked'].includes(operation?.state) ? { uncertain: true } : {}),
1757
- });
1758
- }
1759
- });
1760
-
1761
- app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
1762
- if (!await requireAgentRecoveryForHttp(res)) return;
1763
- const body = req.body || {};
1764
- const workspace = typeof body.workspace === 'string' ? body.workspace : '';
1765
- const requestId = typeof body.requestId === 'string' ? body.requestId.trim() : '';
1766
- if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
1767
- res.status(400).json({ error: 'Project worktree deletion requires a valid requestId' });
1768
- return;
1769
- }
1770
-
1771
- const result = await agentManager.deleteForkWorktreeProject(workspace, {
1772
- force: body.force === true,
1773
- requestId,
1774
- });
1775
- if (result.error) {
1776
- broadcastState();
1777
- const status = result.requiresForce
1778
- ? 409
1779
- : (result.error === 'Workspace not found' || result.error === 'Workspace is required' ? 404 : 400);
1780
- res.status(status).json(result);
1781
- return;
1782
- }
1783
- broadcastState();
1784
- res.json(result);
1785
- });
1786
-
1787
- app.post(routePath(BASE_PATH, '/api/codex/sessions/:sessionId/resume'), express.json(), async (req, res) => {
1788
- await startResumedAgentSession(req, res, 'codex', req.params.sessionId);
1789
- });
1790
-
1791
- app.post(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId/resume'), express.json(), async (req, res) => {
1792
- await startResumedAgentSession(req, res, req.params.provider, req.params.sessionId);
1793
- });
1794
-
1795
- const pendingResumeStarts = new Map();
1796
- const pendingProjectWorkspaceResolutions = new Map();
1797
-
1798
- function resumedAgentStartKey(provider, sessionId, options = {}) {
1799
- return [
1800
- provider,
1801
- options.providerHomeId || 'default',
1802
- sessionId,
1803
- options.fork === true ? 'fork' : 'resume',
1804
- options.asMain === true ? 'main' : 'agent',
1805
- ].join(':');
1806
- }
1807
-
1808
- function findResumedAgent(provider, sessionId, providerHomeId = '') {
1809
- return findActiveAgentClaimingSession(agentManager.getState().agents, provider, { id: sessionId, providerHomeId });
1810
- }
1811
-
1812
- function isMainAgentSessionWorkspace(session) {
1813
- const values = [session && session.cwd, session && session.workspace];
1814
- return values.some(value => {
1815
- const normalized = String(value || '').trim().replace(/[\\/]+$/, '');
1816
- return normalized === '~/.farming' || /(^|[/\\])\.farming$/.test(normalized);
1817
- });
1818
- }
1819
-
1820
- function rememberMainPageAgentSession(provider, sessionId, providerHomeId = '') {
1821
- const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
1822
- if (typeof configManager.rememberMainPageSessionKey === 'function') {
1823
- configManager.rememberMainPageSessionKey(sessionKey, {
1824
- provider,
1825
- providerSessionId: sessionId,
1826
- providerSessionKey: sessionKey,
1827
- providerHomeId: providerHomeId || 'default',
1828
- source: 'resume',
1829
- });
1830
- return;
1831
- }
1832
- const currentKeys = typeof configManager.getMainPageSessionKeys === 'function'
1833
- ? configManager.getMainPageSessionKeys()
1834
- : (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []);
1835
- configManager.updateSettings({
1836
- mainPageSessionKeys: [
1837
- sessionKey,
1838
- ...currentKeys.filter(key => key !== sessionKey),
1839
- ],
1840
- });
1841
- }
1842
-
1843
- function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
1844
- const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
1845
- if (typeof configManager.removeMainPageSessionKey === 'function') {
1846
- configManager.removeMainPageSessionKey(sessionKey);
1847
- return;
1848
- }
1849
- const currentKeys = typeof configManager.getMainPageSessionKeys === 'function'
1850
- ? configManager.getMainPageSessionKeys()
1851
- : (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []);
1852
- if (!currentKeys.includes(sessionKey)) return;
1853
- configManager.updateSettings({
1854
- mainPageSessionKeys: currentKeys.filter(key => key !== sessionKey),
1855
- });
1856
- }
1857
-
1858
- function savedFarmingAgentSession(provider, sessionId, providerHomeId = '') {
1859
- if (typeof configManager.getAgentSessionRecordForProviderSessionKey !== 'function') return null;
1860
- const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
1861
- return sessionKey
1862
- ? configManager.getAgentSessionRecordForProviderSessionKey(sessionKey)
1863
- : null;
1864
- }
1865
-
1866
- async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1867
- const normalizedProvider = normalizeProvider(provider);
1868
- const sessionId = String(rawSessionId || '').trim();
1869
- const providerHomeId = typeof options.providerHomeId === 'string' && options.providerHomeId.trim() ? options.providerHomeId.trim() : 'default';
1870
- if (!normalizedProvider || !isSafeSessionId(sessionId)) {
1871
- return { error: 'invalid session id', status: 400 };
1872
- }
1873
-
1874
- const shouldFork = options.fork === true;
1875
- const requestedAsMain = options.asMain === true && !shouldFork;
1876
- const shouldRememberMainPageSession = options.rememberMainPageSession !== false && !shouldFork && !requestedAsMain;
1877
- const pendingResumeId = resumedAgentStartKey(normalizedProvider, sessionId, {
1878
- fork: shouldFork,
1879
- asMain: requestedAsMain,
1880
- providerHomeId,
1881
- });
1882
- if (!shouldFork) {
1883
- const existingAgent = findResumedAgent(normalizedProvider, sessionId, providerHomeId);
1884
- if (existingAgent) {
1885
- if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1886
- const projectWorkspace = requestedAsMain
1887
- ? ''
1888
- : await canonicalProjectWorkspace(
1889
- existingAgent.gitWorktree?.workspace || existingAgent.projectWorkspace || existingAgent.cwd
1890
- );
1891
- return { agentId: existingAgent.id, projectWorkspace, reused: true };
1892
- }
1893
- }
1894
- const pendingStart = pendingResumeStarts.get(pendingResumeId);
1895
- if (pendingStart) {
1896
- const result = await pendingStart;
1897
- if (result.error) {
1898
- return result;
1899
- }
1900
- if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1901
- return {
1902
- agentId: result.agentId,
1903
- projectWorkspace: result.projectWorkspace || '',
1904
- reused: true,
1905
- pending: true,
1906
- ...(result.claimed ? { claimed: true } : {}),
1907
- };
1908
- }
1909
-
1910
- const startPromise = (async () => {
1911
- let session = await findAgentSession(normalizedProvider, sessionId, {
1912
- limit: 1000,
1913
- providerLimit: 1000,
1914
- scanLimit: 5000,
1915
- providerHomeId,
1916
- providerHomes: configuredProviderHomes(),
1917
- });
1918
- if (
1919
- options.allowUnarchiveArchived === true
1920
- && normalizedProvider === 'codex'
1921
- && !requestedAsMain
1922
- ) {
1923
- const providerHomes = configuredProviderHomes();
1924
- const configuredHomePath = (providerHomes.codex || [])
1925
- .find(home => home.id === providerHomeId)?.path || '';
1926
- const unarchiveResult = await agentManager.ensureCodexSessionAvailable(sessionId, {
1927
- providerHomeId,
1928
- providerHomePath: session?.providerHomePath || configuredHomePath,
1929
- providerHomes,
1930
- cwd: session?.cwd || session?.workspace || '',
1931
- });
1932
- if (unarchiveResult?.error) return unarchiveResult;
1933
- session = await findAgentSession(normalizedProvider, sessionId, {
1934
- limit: 1000,
1935
- providerLimit: 1000,
1936
- scanLimit: 5000,
1937
- providerHomeId,
1938
- providerHomes,
1939
- }) || (session ? { ...session, archived: false } : session);
1940
- }
1941
- if (session && session.archived && !shouldFork) {
1942
- forgetMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1943
- return {
1944
- error: `${session.providerName || normalizedProvider} session is archived. Unarchive it before resuming.`,
1945
- status: 409,
1946
- archived: true,
1947
- };
1948
- }
1949
- if (!shouldFork && !requestedAsMain) {
1950
- const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, normalizedProvider, {
1951
- id: sessionId,
1952
- providerHomeId,
1953
- ...(session || {}),
1954
- });
1955
- if (claimingAgent) {
1956
- if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1957
- const projectWorkspace = await canonicalProjectWorkspace(
1958
- claimingAgent.gitWorktree?.workspace || claimingAgent.projectWorkspace || claimingAgent.cwd
1959
- );
1960
- return { agentId: claimingAgent.id, projectWorkspace, reused: true, claimed: true };
1961
- }
1962
- }
1963
-
1964
- const resumeAsMain = requestedAsMain && isMainAgentSessionWorkspace(session);
1965
- if (requestedAsMain && !resumeAsMain) {
1966
- return { error: 'session is not a Main Agent session', status: 400 };
1967
- }
1968
-
1969
- const savedSession = shouldFork
1970
- ? null
1971
- : savedFarmingAgentSession(normalizedProvider, sessionId, session
1972
- ? (session.providerHomeId || providerHomeId)
1973
- : providerHomeId);
1974
- const hasRequestedCustomTitle = Object.prototype.hasOwnProperty.call(options, 'customTitle');
1975
- const savedAttentionSeq = Number(savedSession?.attentionSeq) || 0;
1976
- const savedReadAttentionSeq = Number(savedSession?.readAttentionSeq) || 0;
1977
- const workingDirectory = session && (session.cwd || session.workspace) ? (session.cwd || session.workspace) : null;
1978
- const projectWorkspace = resumeAsMain
1979
- ? ''
1980
- : await canonicalProjectWorkspace(
1981
- savedSession?.projectWorkspace || (session ? (session.workspace || session.cwd || '') : workingDirectory)
1982
- );
1983
- const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
1984
- fork: shouldFork,
1985
- cwd: workingDirectory,
1986
- modelProvider: normalizedProvider === 'codex'
1987
- ? resolveCodexResumeModelProvider(session ? session.providerHomePath : '')
1988
- : '',
1989
- });
1990
-
1991
- if (!command) {
1992
- return { error: 'invalid session id', status: 400 };
1993
- }
1994
-
1995
- return new Promise((resolve) => {
1996
- const resolvedProviderHomeId = session ? (session.providerHomeId || providerHomeId) : providerHomeId;
1997
- const resumeSource = resumedAgentSource(normalizedProvider, sessionId, resolvedProviderHomeId);
1998
- const startResult = agentManager.startAgent(command, workingDirectory, (agentId, error) => {
1999
- if (error) {
2000
- resolve({ error, status: 400 });
2001
- return;
2002
- }
2003
-
2004
- if (!agentId) {
2005
- resolve({ error: 'failed to resume agent session', status: 500 });
2006
- return;
2007
- }
2008
-
2009
- resolve({ agentId, projectWorkspace });
2010
- }, {
2011
- wantsMain: resumeAsMain,
2012
- task: savedSession?.task || (session ? session.title : ''),
2013
- workflowTemplate: savedSession?.workflowTemplate || '',
2014
- customTitle: hasRequestedCustomTitle
2015
- ? (typeof options.customTitle === 'string' ? options.customTitle : '')
2016
- : (savedSession?.customTitle || ''),
2017
- customTitleExplicit: hasRequestedCustomTitle,
2018
- requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
2019
- projectWorkspace,
2020
- source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
2021
- agentRuntimeMode: ['chat', 'acp'].includes(options.agentRuntimeMode) ? 'chat' : 'terminal',
2022
- acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
2023
- providerHomeId: resolvedProviderHomeId,
2024
- providerHomePath: session ? (session.providerHomePath || '') : '',
2025
- providerSessionTitle: session?.title || savedSession?.providerSessionTitle || '',
2026
- persistentSessionId: savedSession?.id || '',
2027
- pinned: savedSession?.pinned === true,
2028
- projectOrder: savedSession?.projectOrder,
2029
- pinnedOrder: savedSession?.pinnedOrder,
2030
- attentionSeq: savedAttentionSeq,
2031
- readAttentionSeq: savedReadAttentionSeq,
2032
- attentionUpdatedAt: savedSession?.attentionUpdatedAt,
2033
- readAttentionAt: savedSession?.readAttentionAt,
2034
- attentionReason: savedSession?.attentionReason,
2035
- attentionOutputEpoch: savedSession?.attentionOutputEpoch,
2036
- attentionOutputSeq: savedSession?.attentionOutputSeq,
2037
- readOutputEpoch: savedSession?.readOutputEpoch,
2038
- readOutputSeq: savedSession?.readOutputSeq,
2039
- autoReadInitialAttention: options.autoReadInitialAttention === true
2040
- && savedAttentionSeq <= savedReadAttentionSeq,
2041
- preserveProviderSessionProfile: normalizedProvider === 'codex',
2042
- });
2043
- Promise.resolve(startResult).catch((error) => {
2044
- resolve({ error: error.message || 'failed to resume agent session', status: 500 });
2045
- });
2046
- });
2047
- })();
2048
- pendingResumeStarts.set(pendingResumeId, startPromise);
2049
-
2050
- const result = await startPromise;
2051
- if (pendingResumeStarts.get(pendingResumeId) === startPromise) {
2052
- pendingResumeStarts.delete(pendingResumeId);
2053
- }
2054
-
2055
- if (result.error) {
2056
- return result;
2057
- }
2058
-
2059
- if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
2060
- if (result.reused) {
2061
- return {
2062
- agentId: result.agentId,
2063
- projectWorkspace: result.projectWorkspace || '',
2064
- reused: true,
2065
- ...(result.claimed ? { claimed: true } : {}),
2066
- };
2067
- }
2068
- return {
2069
- agentId: result.agentId,
2070
- projectWorkspace: result.projectWorkspace || '',
2071
- };
2072
- }
2073
-
2074
- async function canonicalProjectWorkspace(workspace) {
2075
- const candidate = configManager.expandWorkspacePath(String(workspace || '').trim());
2076
- if (!candidate) return '';
2077
- const pending = pendingProjectWorkspaceResolutions.get(candidate);
2078
- if (pending) return pending;
2079
-
2080
- const resolution = (async () => {
2081
- try {
2082
- const worktree = await inspectGitWorktree(candidate);
2083
- if (worktree?.workspace) return worktree.workspace;
2084
- } catch (error) {
2085
- console.warn('Failed to resolve project worktree:', candidate, error?.message || error);
2086
- }
2087
- try {
2088
- return fs.realpathSync(path.resolve(candidate));
2089
- } catch {
2090
- return candidate;
2091
- }
2092
- })();
2093
- pendingProjectWorkspaceResolutions.set(candidate, resolution);
2094
- try {
2095
- return await resolution;
2096
- } finally {
2097
- if (pendingProjectWorkspaceResolutions.get(candidate) === resolution) {
2098
- pendingProjectWorkspaceResolutions.delete(candidate);
2099
- }
2100
- }
2101
- }
2102
-
2103
- async function startResumedAgentSession(req, res, provider, rawSessionId) {
2104
- const shouldFork = req.body && req.body.fork === true;
2105
- const requestedAsMain = req.body && req.body.asMain === true && !shouldFork;
2106
- const allowUnarchiveArchived = req.body && req.body.unarchiveArchived === true && !shouldFork && !requestedAsMain;
2107
- const providerHomeId = req.body && typeof req.body.providerHomeId === 'string'
2108
- ? req.body.providerHomeId
2109
- : '';
2110
- const sessionKey = mainPageAgentSessionKey(provider, rawSessionId, providerHomeId);
2111
- const mainPageSessionWasRemembered = !shouldFork
2112
- && !requestedAsMain
2113
- && configManager.getMainPageSessionKeys().includes(sessionKey);
2114
- if (
2115
- req.body
2116
- && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
2117
- && typeof req.body.customTitle !== 'string'
2118
- ) {
2119
- res.status(400).json({ error: 'customTitle must be a string' });
2120
- return;
2121
- }
2122
- const result = await resumeAgentSessionById(provider, rawSessionId, {
2123
- fork: shouldFork,
2124
- asMain: requestedAsMain,
2125
- allowUnarchiveArchived,
2126
- providerHomeId,
2127
- ...(req.body && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
2128
- ? { customTitle: req.body.customTitle }
2129
- : {}),
2130
- agentRuntimeMode: req.body && ['chat', 'acp'].includes(req.body.agentRuntimeMode) ? 'chat' : 'terminal',
2131
- acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
2132
- });
2133
-
2134
- if (result.error) {
2135
- res.status(result.status || 400).json({ error: result.error });
2136
- return;
2137
- }
2138
-
2139
- let projectMembership = {
2140
- projectWorkspaces: configManager.getSettings().projectWorkspaces || [],
2141
- pinnedProjectWorkspaces: configManager.getSettings().pinnedProjectWorkspaces || [],
2142
- };
2143
- try {
2144
- if (!requestedAsMain && result.projectWorkspace) {
2145
- projectMembership = configManager.mountProjectWorkspace(result.projectWorkspace);
2146
- }
2147
- } catch (error) {
2148
- let rollbackError = '';
2149
- if (!result.reused) {
2150
- try {
2151
- const rollback = await agentManager.archiveAgent(result.agentId, {
2152
- reason: 'project-mount-failed',
2153
- recordHistory: false,
2154
- requireEngineExit: true,
2155
- scheduleProviderArchive: false,
2156
- });
2157
- if (rollback?.error) rollbackError = rollback.error;
2158
- } catch (cleanupError) {
2159
- rollbackError = cleanupError?.message || String(cleanupError);
2160
- }
2161
- }
2162
- if (!mainPageSessionWasRemembered && !rollbackError) {
2163
- forgetMainPageAgentSession(provider, rawSessionId, providerHomeId || 'default');
2164
- }
2165
- broadcastState();
2166
- const mountError = error.message || 'Failed to create Project';
2167
- res.status(500).json({
2168
- error: rollbackError
2169
- ? `${mountError}. Rollback failed: ${rollbackError}`
2170
- : mountError,
2171
- ...(rollbackError ? { rollbackError } : {}),
2172
- });
2173
- return;
2174
- }
2175
- broadcastState();
2176
- if (result.reused) {
2177
- res.status(200).json({
2178
- agentId: result.agentId,
2179
- ...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
2180
- projectWorkspaces: projectMembership.projectWorkspaces,
2181
- pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
2182
- reused: true,
2183
- ...(result.claimed ? { claimed: true } : {}),
2184
- ...(result.pending ? { pending: true } : {}),
2185
- });
2186
- return;
2187
- }
2188
-
2189
- res.status(201).json({
2190
- agentId: result.agentId,
2191
- ...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
2192
- projectWorkspaces: projectMembership.projectWorkspaces,
2193
- pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
2194
- });
2195
- }
2196
-
2197
- async function autoResumeMainPageAgentSessions() {
2198
- if (typeof agentManager.whenRecovered === 'function') {
2199
- await agentManager.whenRecovered();
2200
- }
2201
-
2202
- const sessions = mainPageAgentSessionsToAutoResume(configManager.getSettings());
2203
- if (sessions.length === 0) return;
2204
-
2205
- let knownSessions;
2206
- try {
2207
- knownSessions = await agentSessionsCache.get(
2208
- JSON.stringify(configManager.getSettings().agentHomes || {}),
2209
- { maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS }
2210
- );
2211
- } catch (error) {
2212
- console.warn('Failed to load Agent session catalog for auto-resume:', error && (error.message || error));
2213
- return;
2214
- }
2215
- const knownSessionByKey = new Map(knownSessions.map(session => [
2216
- mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId || 'default'),
2217
- session,
2218
- ]));
2219
-
2220
- let resumedCount = 0;
2221
- for (const session of sessions) {
2222
- try {
2223
- const sessionDetails = knownSessionByKey.get(mainPageAgentSessionKey(
2224
- session.provider,
2225
- session.sessionId,
2226
- session.providerHomeId || 'default'
2227
- ));
2228
- if (!sessionDetails) {
2229
- console.warn('Dropping stale main-page session from auto-resume:', session.provider, session.sessionId);
2230
- forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
2231
- continue;
2232
- }
2233
-
2234
- const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, session.provider, {
2235
- id: session.sessionId,
2236
- ...(sessionDetails || {}),
2237
- });
2238
- if (claimingAgent) {
2239
- continue;
2240
- }
2241
-
2242
- const result = await resumeAgentSessionById(session.provider, session.sessionId, {
2243
- rememberMainPageSession: false,
2244
- providerHomeId: session.providerHomeId || 'default',
2245
- autoReadInitialAttention: true,
2246
- });
2247
- if (result.error) {
2248
- const message = String(result.error || '').toLowerCase();
2249
- if (session.provider === 'qoder' && message.includes('invalid session identifier')) {
2250
- console.warn('Dropping stale qoder session from auto-resume:', session.provider, session.sessionId, result.error);
2251
- forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
2252
- continue;
2253
- }
2254
- console.warn('Failed to auto-resume main page agent session:', session.provider, session.sessionId, result.error);
2255
- } else {
2256
- resumedCount += 1;
2257
- }
2258
- } catch (error) {
2259
- const message = error instanceof Error ? error.message : String(error);
2260
- console.warn('Failed to auto-resume main page agent session:', session.provider, session.sessionId, message);
2261
- }
2262
- }
2263
-
2264
- if (resumedCount > 0) {
2265
- broadcastState();
2266
- }
2267
- }
2268
-
2269
- app.post(routePath(BASE_PATH, '/api/settings'), express.json(), async (req, res) => {
2270
- const settingsPatch = { ...(req.body || {}) };
2271
- delete settingsPatch.mainPageSessionKeys;
2272
- delete settingsPatch.projectWorkspaces;
2273
- delete settingsPatch.pinnedProjectWorkspaces;
2274
- const browserConfigurationKeys = [
2275
- 'browserSource',
2276
- 'browserExecutablePath',
2277
- 'browserExternalCdpUrl',
2278
- ];
2279
- const changesBrowserExtension = Object.prototype.hasOwnProperty.call(settingsPatch, 'browserExtensionEnabled');
2280
- const changesBrowserConfiguration = browserConfigurationKeys.some(key =>
2281
- Object.prototype.hasOwnProperty.call(settingsPatch, key)
2282
- );
2283
- const currentSettings = configManager.getSettings();
2284
- const browserExtensionEnabled = settingsPatch.browserExtensionEnabled === true;
2285
- const desiredBrowserEnabled = changesBrowserExtension
2286
- ? browserExtensionEnabled
2287
- : currentSettings.browserExtensionEnabled === true;
2288
- if ((changesBrowserExtension && browserExtensionEnabled) || changesBrowserConfiguration) {
2289
- const desiredSelection = browserResourceManager.browserSelection({
2290
- ...currentSettings,
2291
- ...settingsPatch,
2292
- });
2293
- const probe = await browserResourceManager.probeCapability(desiredSelection);
2294
- if (
2295
- (changesBrowserConfiguration || desiredBrowserEnabled)
2296
- && (!probe.runtimeCapability || probe.runtimeCapability.error)
2297
- ) {
2298
- res.status(400).json({
2299
- error: probe.runtimeCapability?.error
2300
- || 'Install a Chromium-based browser or configure a loopback external CDP endpoint',
2301
- code: 'BROWSER_EXECUTABLE_NOT_FOUND',
2302
- });
2303
- return;
2304
- }
2305
- }
2306
- if (
2307
- currentSettings.browserExtensionEnabled === true
2308
- && (
2309
- (changesBrowserExtension && !browserExtensionEnabled)
2310
- || changesBrowserConfiguration
2311
- )
2312
- ) {
2313
- try {
2314
- await browserResourceManager.stopAll();
2315
- } catch (error) {
2316
- res.status(Number(error?.status) || 500).json({
2317
- error: error?.message || 'Browser extension could not be disabled',
2318
- code: error?.code || 'BROWSER_DISABLE_FAILED',
2319
- });
2320
- return;
2321
- }
2322
- }
2323
- configManager.updateSettings(settingsPatch);
2324
- if (changesBrowserExtension || changesBrowserConfiguration) {
2325
- await browserResourceManager.refreshCapability();
2326
- }
2327
- agentSessionsCache.invalidate();
2328
- res.json({
2329
- success: true,
2330
- settings: configManager.getSettings()
2331
- });
2332
- scheduleBroadcastState();
2333
- });
2334
-
2335
- app.post(routePath(BASE_PATH, '/api/themes/:themeId/set'), express.json(), (req, res) => {
2336
- const theme = themeManager.getTheme(req.params.themeId);
2337
- if (!theme) {
2338
- res.status(404).json({ error: 'Theme not found' });
2339
- return;
2340
- }
2341
-
2342
- configManager.updateSettings({ theme: req.params.themeId });
2343
- res.json({ success: true, theme: req.params.themeId });
2344
- });
2345
-
2346
- app.get(routePath(BASE_PATH, '/api/themes/:themeId/settings'), (req, res) => {
2347
- const theme = themeManager.getTheme(req.params.themeId);
2348
- if (!theme) {
2349
- res.status(404).json({ error: 'Theme not found' });
2350
- return;
2351
- }
2352
-
2353
- const settings = themeManager.getThemeSettings(req.params.themeId);
2354
- res.json({ settings });
2355
- });
2356
-
2357
- app.post(routePath(BASE_PATH, '/api/themes/:themeId/settings'), express.json(), (req, res) => {
2358
- const theme = themeManager.getTheme(req.params.themeId);
2359
- if (!theme) {
2360
- res.status(404).json({ error: 'Theme not found' });
2361
- return;
2362
- }
2363
-
2364
- const success = themeManager.updateThemeSettings(req.params.themeId, req.body);
2365
- if (success) {
2366
- res.json({ success: true, settings: themeManager.getThemeSettings(req.params.themeId) });
2367
- } else {
2368
- res.status(500).json({ error: 'Failed to update theme settings' });
2369
- }
2370
- });
2371
-
2372
- app.get(routePath(BASE_PATH, '/api/themes/:themeId'), (req, res) => {
2373
- const theme = themeManager.getTheme(req.params.themeId);
2374
- if (!theme) {
2375
- res.status(404).json({ error: 'Theme not found' });
2376
- return;
2377
- }
2378
-
2379
- const css = themeManager.getThemeCSS(req.params.themeId);
2380
- res.json({
2381
- theme,
2382
- css
2383
- });
2384
- });
2385
-
2386
- wss.on('connection', (ws, req) => {
2387
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
2388
- const viewerPrefix = routePath(BASE_PATH, '/api/browsers/');
2389
- if (url.pathname.startsWith(viewerPrefix) && url.pathname.endsWith('/viewer')) {
2390
- if (authEnabled && !tokenAuth.verifyWebSocket(req)) {
2391
- ws.close(4001, 'Authentication required');
2392
- return;
2393
- }
2394
- const encodedId = url.pathname.slice(viewerPrefix.length, -'/viewer'.length);
2395
- try {
2396
- browserResourceManager.attachViewer(decodeURIComponent(encodedId), ws);
2397
- } catch (error) {
2398
- ws.close(4004, error?.message || 'Browser resource not found');
2399
- }
2400
- return;
2401
- }
2402
- if (url.pathname !== WS_PATH) {
2403
- ws.close(1008, 'Invalid path');
2404
- return;
2405
- }
2406
-
2407
- // Verify token for WebSocket connections when auth is enabled.
2408
- if (authEnabled && !tokenAuth.verifyWebSocket(req)) {
2409
- ws.close(4001, 'Authentication required');
2410
- return;
2411
- }
2412
-
2413
- console.log('Client connected');
2414
- ws.connectionId = crypto.randomUUID();
2415
-
2416
- ws.on('message', (message) => {
2417
- try {
2418
- const data = JSON.parse(message);
2419
- const validation = validateClientMessage(data);
2420
- if (!validation.ok) {
2421
- ws.send(JSON.stringify({
2422
- type: 'protocol-error',
2423
- protocolVersion: PROTOCOL_VERSION,
2424
- requestId: typeof data?.requestId === 'string' ? data.requestId : '',
2425
- message: validation.error,
2426
- }));
2427
- return;
2428
- }
2429
- handleMessage(ws, data);
2430
- } catch (e) {
2431
- console.error('Failed to parse message:', e);
2432
- ws.send(JSON.stringify({
2433
- type: 'protocol-error',
2434
- protocolVersion: PROTOCOL_VERSION,
2435
- requestId: '',
2436
- message: 'message must be valid JSON',
2437
- }));
2438
- }
2439
- });
2440
-
2441
- ws.on('close', () => {
2442
- clearWorkspaceFileWatch(ws);
2443
- console.log('Client disconnected');
2444
- });
2445
-
2446
- ws.send(JSON.stringify({
2447
- type: 'protocol-hello',
2448
- protocolVersion: PROTOCOL_VERSION,
2449
- minProtocolVersion: MIN_PROTOCOL_VERSION,
2450
- }));
2451
- sendState(ws);
2452
- });
2453
-
2454
- const MAIN_AGENT_RESTART_COMMANDS = new Set(['codex', 'claude', 'opencode', 'qoder', 'bash', 'zsh']);
2455
-
2456
- function normalizeMainAgentRestartCommand(command) {
2457
- const normalized = String(command || '').trim();
2458
- return MAIN_AGENT_RESTART_COMMANDS.has(normalized) ? normalized : '';
2459
- }
2460
-
2461
- function restartMainAgent(ws, command) {
2462
- const normalizedCommand = normalizeMainAgentRestartCommand(command);
2463
- if (!normalizedCommand) {
2464
- ws.send(JSON.stringify({ type: 'error', message: 'Unsupported Main Agent restart command' }));
2465
- return;
2466
- }
2467
-
2468
- void (async () => {
2469
- try {
2470
- const state = agentManager.getState();
2471
- const currentMain = state.agents.find(agent => (
2472
- agent.id === state.mainAgentId || agent.isMain === true
2473
- ));
2474
- if (currentMain) {
2475
- const killed = await agentManager.killAgent(currentMain.id);
2476
- if (killed?.error) {
2477
- ws.send(JSON.stringify({ type: 'error', message: killed.error }));
2478
- return;
2479
- }
2480
- }
2481
-
2482
- await agentManager.startAgent(normalizedCommand, null, (agentId, error) => {
2483
- if (error) {
2484
- ws.send(JSON.stringify({ type: 'error', message: error }));
2485
- } else if (agentId) {
2486
- ws.agentId = agentId;
2487
- broadcastState();
2488
- ws.send(JSON.stringify({ type: 'agent-started', agentId }));
2489
- }
2490
- }, {
2491
- wantsMain: true
2492
- });
2493
- } catch (error) {
2494
- const message = error instanceof Error ? error.message : 'Failed to restart Main Agent';
2495
- ws.send(JSON.stringify({ type: 'error', message }));
2496
- broadcastState();
2497
- }
2498
- })();
2499
- }
2500
-
2501
- async function killAgentFromMessage(ws, agentId, options = {}) {
2502
- try {
2503
- const requested = await agentManager.requestKillAgent(agentId, {
2504
- acknowledgeUnprovenAcpExit: options.acknowledgeUnprovenAcpExit === true,
2505
- });
2506
- const result = requested.result;
2507
- if (result?.error) {
2508
- ws.send(JSON.stringify({ type: 'error', message: result.error }));
2509
- } else if (result?.accepted) {
2510
- ws.send(JSON.stringify({ type: 'agent-delete-accepted', ...result }));
2511
- void requested.completion.then(completed => {
2512
- if (completed?.error && ws.readyState === WebSocket.OPEN) {
2513
- ws.send(JSON.stringify({ type: 'error', message: completed.error }));
2514
- }
2515
- }).catch(error => {
2516
- if (ws.readyState === WebSocket.OPEN) {
2517
- ws.send(JSON.stringify({
2518
- type: 'error',
2519
- message: error instanceof Error ? error.message : 'Failed to stop Agent',
2520
- }));
2521
- }
2522
- }).finally(() => broadcastState());
2523
- }
2524
- } catch (error) {
2525
- ws.send(JSON.stringify({
2526
- type: 'error',
2527
- message: error instanceof Error ? error.message : 'Failed to stop Agent',
2528
- }));
2529
- } finally {
2530
- broadcastState();
2531
- }
2532
- }
2533
-
2534
- async function sendInputMessage(ws, data) {
2535
- const targetAgentId = resolveInputTargetAgentId(ws, data);
2536
- if (!targetAgentId) return;
2537
-
2538
- const inputParts = inputPartsFromMessage(data);
2539
- if (inputParts.length === 0) return;
2540
- await agentManager.sendInput(targetAgentId, inputParts);
2541
- }
2542
-
2543
- async function sendComposerInputMessage(ws, data) {
2544
- const targetAgentId = resolveInputTargetAgentId(ws, data);
2545
- const requestId = typeof data.requestId === 'string' ? data.requestId.trim() : '';
2546
- const responseAgentId = targetAgentId || (typeof data.agentId === 'string' ? data.agentId : '');
2547
- const respond = (accepted, message = '', uncertain = false) => {
2548
- if (!requestId || !responseAgentId) return;
2549
- ws.send(JSON.stringify({
2550
- type: 'composer-input-result',
2551
- requestId,
2552
- agentId: responseAgentId,
2553
- accepted,
2554
- ...(message ? { message } : {}),
2555
- ...(uncertain ? { uncertain: true } : {}),
2556
- }));
2557
- };
2558
- const message = typeof data.message === 'string' ? data.message : '';
2559
- if (!/^[A-Za-z0-9._:-]{1,160}$/.test(requestId)) {
2560
- if (responseAgentId) {
2561
- ws.send(JSON.stringify({
2562
- type: 'composer-input-result',
2563
- requestId: requestId || 'invalid-request',
2564
- agentId: responseAgentId,
2565
- accepted: false,
2566
- message: 'Structured Composer input requires a valid requestId',
2567
- }));
2568
- }
2569
- return;
2570
- }
2571
- const content = [];
2572
- if (message.trim()) content.push({ type: 'text', text: message });
2573
- const attachmentsRoot = path.resolve(imageAttachmentsDir());
2574
- const attachments = Array.isArray(data.attachments) ? data.attachments.slice(0, 8) : [];
2575
- for (const attachment of attachments) {
2576
- if (!['image', 'audio'].includes(attachment?.kind) || typeof attachment.path !== 'string') continue;
2577
- const filePath = path.resolve(attachment.path);
2578
- if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) continue;
2579
- const mimeType = typeof attachment.type === 'string' && attachment.kind === 'image' && /^image\/(?:png|jpe?g|gif|webp)$/i.test(attachment.type)
2580
- ? attachment.type.toLowerCase()
2581
- : (typeof attachment.type === 'string' && attachment.kind === 'audio' && Object.hasOwn(AUDIO_ATTACHMENT_EXTENSIONS, attachment.type.toLowerCase())
2582
- ? attachment.type.toLowerCase()
2583
- : '');
2584
- if (!mimeType) continue;
2585
- try {
2586
- const data = await fs.promises.readFile(filePath);
2587
- if (data.length === 0 || data.length > 12 * 1024 * 1024) continue;
2588
- content.push({
2589
- type: attachment.kind,
2590
- data: data.toString('base64'),
2591
- mimeType,
2592
- path: filePath,
2593
- uri: pathToFileURL(filePath).href,
2594
- });
2595
- } catch {
2596
- // The text fallback already explains failed or unavailable uploads.
2597
- }
2598
- }
2599
- if (!targetAgentId || content.length === 0) {
2600
- respond(false, 'Composer message is empty');
2601
- return;
2602
- }
2603
- const targetAgent = agentManager.getState().agents.find(agent => agent.id === targetAgentId);
2604
- const structuredRuntime = targetAgent && runtimeKind(targetAgent) !== 'terminal';
2605
- if (!structuredRuntime) {
2606
- const error = 'Terminal Composer input requires the active terminal owner';
2607
- if (requestId) respond(false, error);
2608
- else ws.send(JSON.stringify({ type: 'error', message: error }));
2609
- return;
2610
- }
2611
- try {
2612
- await agentManager.sendComposerMessage(targetAgentId, content, { requestId });
2613
- respond(true);
2614
- } catch (error) {
2615
- const message = error && error.message ? error.message : 'Failed to send Composer message';
2616
- if (requestId) respond(false, message, error?.uncertain === true);
2617
- else ws.send(JSON.stringify({ type: 'error', message }));
2618
- }
2619
- }
2620
-
2621
- function handleMessage(ws, data) {
2622
- switch (data.type) {
2623
- case 'protocol-hello':
2624
- if (!protocolCompatible(data.protocolVersion)) {
2625
- ws.close(4002, `Unsupported Farming protocol version ${data.protocolVersion}`);
2626
- return;
2627
- }
2628
- ws.protocolVersion = data.protocolVersion;
2629
- break;
2630
- case 'start-agent': {
2631
- const workspace = data.workspace || null;
2632
- void (async () => {
2633
- const projectWorkspace = data.asMain === true
2634
- ? ''
2635
- : await canonicalProjectWorkspace(
2636
- typeof data.projectWorkspace === 'string' && data.projectWorkspace.trim()
2637
- ? data.projectWorkspace
2638
- : workspace
2639
- );
2640
- agentManager.startAgent(data.command, workspace, (agentId, error) => {
2641
- if (error) {
2642
- ws.send(JSON.stringify({ type: 'error', message: error }));
2643
- } else if (agentId) {
2644
- void (async () => {
2645
- try {
2646
- if (projectWorkspace) configManager.mountProjectWorkspace(projectWorkspace);
2647
- } catch (mountError) {
2648
- let rollbackError = '';
2649
- try {
2650
- const rollback = await agentManager.archiveAgent(agentId, {
2651
- reason: 'project-mount-failed',
2652
- recordHistory: false,
2653
- requireEngineExit: true,
2654
- scheduleProviderArchive: false,
2655
- });
2656
- if (rollback?.error) rollbackError = rollback.error;
2657
- } catch (cleanupError) {
2658
- rollbackError = cleanupError?.message || String(cleanupError);
2659
- }
2660
- broadcastState();
2661
- const errorMessage = mountError.message || 'Failed to create Project';
2662
- ws.send(JSON.stringify({
2663
- type: 'error',
2664
- message: rollbackError
2665
- ? `${errorMessage}. Rollback failed: ${rollbackError}`
2666
- : errorMessage,
2667
- }));
2668
- return;
2669
- }
2670
- ws.agentId = agentId;
2671
- broadcastState();
2672
- ws.send(JSON.stringify({ type: 'agent-started', agentId }));
2673
- })().catch(callbackError => {
2674
- console.warn('Failed to finish started Agent transition:', agentId, callbackError?.message || callbackError);
2675
- });
2676
- }
2677
- }, {
2678
- wantsMain: data.asMain === true,
2679
- projectWorkspace,
2680
- task: typeof data.task === 'string' ? data.task : '',
2681
- workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2682
- customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2683
- createRequestId: typeof data.requestId === 'string' ? data.requestId : '',
2684
- codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2685
- agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2686
- acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
2687
- providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
2688
- ...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
2689
- ...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
2690
- ...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
2691
- });
2692
- })().catch(error => {
2693
- ws.send(JSON.stringify({
2694
- type: 'error',
2695
- message: error.message || 'Failed to resolve Project workspace',
2696
- }));
2697
- });
2698
- break;
2699
- }
2700
- case 'input':
2701
- {
2702
- void sendInputMessage(ws, data);
2703
- }
2704
- break;
2705
-
2706
- case 'composer-input':
2707
- {
2708
- void sendComposerInputMessage(ws, data);
2709
- }
2710
- break;
2711
-
2712
- case 'acp-permission-response':
2713
- try {
2714
- agentManager.respondToAcpPermission(
2715
- data.agentId,
2716
- data.requestId,
2717
- data.optionId,
2718
- data.cancelled === true
2719
- );
2720
- } catch (error) {
2721
- ws.send(JSON.stringify({
2722
- type: 'error',
2723
- message: error && error.message ? error.message : 'Failed to respond to ACP permission',
2724
- }));
2725
- }
2726
- break;
2727
-
2728
- case 'interrupt-agent':
2729
- if (data.agentId) {
2730
- void agentManager.interruptAgent(data.agentId);
2731
- }
2732
- break;
2733
-
2734
- case 'focus-agent':
2735
- ws.focusedAgentId = data.agentId;
2736
- if (data.streamScope === 'focused' || data.streamScope === 'all') {
2737
- ws.streamScope = data.streamScope;
2738
- }
2739
- if (data.previewScope === 'none' || data.previewScope === 'focused' || data.previewScope === 'all') {
2740
- ws.previewScope = data.previewScope;
2741
- }
2742
- if (data.refreshState === true) {
2743
- sendState(ws);
2744
- }
2745
- break;
2746
-
2747
- case 'resize-agent':
2748
- if (data.agentId && Number.isFinite(data.cols) && Number.isFinite(data.rows)) {
2749
- agentManager.requestAgentSessionResize(data.agentId, data.cols, data.rows);
2750
- }
2751
- break;
2752
-
2753
- case 'clear-terminal':
2754
- if (data.agentId) void agentManager.clearAgentSessionBuffer(data.agentId);
2755
- break;
2756
-
2757
- case 'watch-workspace-files':
2758
- watchWorkspaceFiles(ws, data);
2759
- break;
2760
-
2761
- case 'unwatch-workspace-files':
2762
- clearWorkspaceFileWatch(ws, data.agentId);
2763
- break;
2764
-
2765
- case 'kill-agent':
2766
- void killAgentFromMessage(ws, data.agentId, {
2767
- acknowledgeUnprovenAcpExit: data.acknowledgeUnprovenAcpExit === true,
2768
- });
2769
- break;
2770
-
2771
- case 'restart-main-agent':
2772
- restartMainAgent(ws, data.command);
2773
- break;
2774
-
2775
- default:
2776
- console.log('Unknown message type:', data.type);
2777
- }
2778
- }
2779
-
2780
- const { resolveInputTargetAgentId } = require('./input-routing');
2781
-
2782
- function sendWorkspaceFileWatchError(ws, error) {
2783
- if (ws.readyState !== WebSocket.OPEN) return;
2784
- const message = error instanceof WorkspaceFileError ? error.message : 'failed to watch workspace files';
2785
- ws.send(JSON.stringify({ type: 'error', message }));
2786
- }
2787
-
2788
- function isCurrentWorkspaceFileWatch(ws, agentId, record) {
2789
- return Boolean(
2790
- !record.cancelled
2791
- && ws.workspaceFileUnsubscribes?.get(agentId) === record
2792
- );
2793
- }
2794
-
2795
- function closeWorkspaceFileWatchRecord(record) {
2796
- record.cancelled = true;
2797
- if (!record.unsubscribe) return;
2798
- Promise.resolve(record.unsubscribe()).catch((error) => {
2799
- console.error('Failed to clear workspace file watch:', error);
2800
- });
2801
- }
2802
-
2803
- function clearWorkspaceFileWatch(ws, agentId = null) {
2804
- const watches = ws.workspaceFileUnsubscribes;
2805
- if (!watches) return;
2806
-
2807
- const entries = agentId
2808
- ? [[agentId, watches.get(agentId)]]
2809
- : Array.from(watches.entries());
2810
-
2811
- entries.forEach(([watchedAgentId, record]) => {
2812
- if (!record) return;
2813
- if (watches.get(watchedAgentId) === record) watches.delete(watchedAgentId);
2814
- closeWorkspaceFileWatchRecord(record);
2815
- });
2816
-
2817
- if (watches.size === 0) {
2818
- ws.workspaceFileUnsubscribes = null;
2819
- }
2820
- }
2821
-
2822
- async function watchWorkspaceFiles(ws, data) {
2823
- try {
2824
- if (!data.agentId) {
2825
- throw new WorkspaceFileError('agentId is required', 400);
2826
- }
2827
- if (!ws.workspaceFileUnsubscribes) {
2828
- ws.workspaceFileUnsubscribes = new Map();
2829
- }
2830
- const watches = ws.workspaceFileUnsubscribes;
2831
- const existing = watches.get(data.agentId);
2832
- if (existing) {
2833
- const watching = await existing.ready;
2834
- if (watching && isCurrentWorkspaceFileWatch(ws, data.agentId, existing) && ws.readyState === WebSocket.OPEN) {
2835
- ws.send(JSON.stringify({
2836
- type: 'workspace-file-watch',
2837
- agentId: data.agentId,
2838
- watching: true,
2839
- }));
2840
- }
2841
- return;
2842
- }
2843
-
2844
- const root = resolveWorkspaceRoot(agentManager, data.agentId);
2845
- const record = {
2846
- cancelled: false,
2847
- unsubscribe: null,
2848
- ready: null,
2849
- };
2850
- record.ready = (async () => {
2851
- const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
2852
- if (!isCurrentWorkspaceFileWatch(ws, data.agentId, record) || ws.readyState !== WebSocket.OPEN) return;
2853
- ws.send(JSON.stringify({
2854
- type: 'workspace-file-event',
2855
- event: {
2856
- agentId: data.agentId,
2857
- ...event,
2858
- },
2859
- }));
2860
- });
2861
- record.unsubscribe = unsubscribe;
2862
- if (!isCurrentWorkspaceFileWatch(ws, data.agentId, record) || ws.readyState !== WebSocket.OPEN) {
2863
- closeWorkspaceFileWatchRecord(record);
2864
- return false;
2865
- }
2866
- return true;
2867
- })();
2868
- watches.set(data.agentId, record);
2869
- try {
2870
- const watching = await record.ready;
2871
- if (watching && isCurrentWorkspaceFileWatch(ws, data.agentId, record) && ws.readyState === WebSocket.OPEN) {
2872
- ws.send(JSON.stringify({
2873
- type: 'workspace-file-watch',
2874
- agentId: data.agentId,
2875
- watching: true,
2876
- }));
2877
- }
2878
- } catch (error) {
2879
- if (watches.get(data.agentId) === record) watches.delete(data.agentId);
2880
- closeWorkspaceFileWatchRecord(record);
2881
- if (watches.size === 0 && ws.workspaceFileUnsubscribes === watches) {
2882
- ws.workspaceFileUnsubscribes = null;
2883
- }
2884
- throw error;
2885
- }
2886
- } catch (error) {
2887
- sendWorkspaceFileWatchError(ws, error);
2888
- }
2889
- }
2890
-
2891
- function buildStatePayload() {
2892
- const state = agentManager.getState();
2893
- const settings = configManager.getSettings();
2894
- return {
2895
- ...state,
2896
- agents: state.agents.map(agent => ({
2897
- ...agent,
2898
- workspaceRootId: rootIdForPath(agent.projectWorkspace || agent.gitWorktree?.workspace || agent.cwd),
2899
- })),
2900
- workspaceRoots: workspaceRootRegistry.list(),
2901
- mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
2902
- ? configManager.getMainPageSessionKeys()
2903
- : (Array.isArray(settings.mainPageSessionKeys) ? settings.mainPageSessionKeys : []),
2904
- projectWorkspaces: settings.projectWorkspaces || [],
2905
- pinnedProjectWorkspaces: settings.pinnedProjectWorkspaces || [],
2906
- };
2907
- }
2908
-
2909
- function sendState(ws) {
2910
- const state = buildStatePayload();
2911
- ws.send(JSON.stringify({ type: 'state', state }));
2912
- agentManager.getPreviewPayloads().forEach((preview) => {
2913
- sendPreview(ws, preview);
2914
- });
2915
- }
2916
-
2917
- function previewForClient(preview, client) {
2918
- if (!preview || !client || client.focusedAgentId !== preview.agentId || !preview.previewSnapshot) {
2919
- return preview;
2920
- }
2921
- // The active terminal is hydrated from its authoritative session-view and
2922
- // receives live session-output. Sending its sidebar snapshot again adds a
2923
- // large, unrelated React update to every keystroke in a full-screen TUI.
2924
- return {
2925
- ...preview,
2926
- previewSnapshot: null,
2927
- };
2928
- }
2929
-
2930
- function sendPreview(ws, preview) {
2931
- ws.send(JSON.stringify({
2932
- type: 'session-preview',
2933
- preview: previewForClient(preview, ws),
2934
- }));
2935
- }
2936
-
2937
- const STATE_BROADCAST_INTERVAL_MS = 120;
2938
- const PREVIEW_BROADCAST_INTERVAL_MS = 500;
2939
- const SESSION_STREAM_BROADCAST_INTERVAL_MS = 33;
2940
- const AGENT_ACTIVITY_BROADCAST_DELAY_MS = SESSION_STREAM_BROADCAST_INTERVAL_MS;
2941
- const MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
2942
- let stateBroadcastTimer = null;
2943
- let lastStateBroadcastAt = 0;
2944
- const pendingPreviewBroadcasts = new Map();
2945
- const pendingAgentActivityBroadcasts = new Map();
2946
- const pendingAgentUpdates = new Map();
2947
- const pendingSessionStreams = new Map();
2948
- let sessionStreamBroadcastTimer = null;
2949
- let lastSessionStreamBroadcastAt = 0;
2950
-
2951
- function broadcastState() {
2952
- lastStateBroadcastAt = Date.now();
2953
- stateBroadcastTimer = null;
2954
- const state = buildStatePayload();
2955
- const message = JSON.stringify({ type: 'state', state });
2956
- wss.clients.forEach(client => {
2957
- if (client.readyState === WebSocket.OPEN) {
2958
- client.send(message);
2959
- }
2960
- });
2961
- }
2962
-
2963
- function scheduleBroadcastState() {
2964
- const now = Date.now();
2965
- const elapsed = now - lastStateBroadcastAt;
2966
-
2967
- if (elapsed >= STATE_BROADCAST_INTERVAL_MS) {
2968
- broadcastState();
2969
- return;
2970
- }
2971
-
2972
- if (stateBroadcastTimer) {
2973
- return;
2974
- }
2975
-
2976
- stateBroadcastTimer = setTimeout(() => {
2977
- broadcastState();
2978
- }, STATE_BROADCAST_INTERVAL_MS - elapsed);
2979
- }
2980
-
2981
- function broadcastSessionPreview(preview) {
2982
- wss.clients.forEach((client) => {
2983
- const previewAllowed = client.previewScope !== 'none'
2984
- && (client.previewScope !== 'focused' || client.focusedAgentId === preview.agentId);
2985
- if (client.readyState === WebSocket.OPEN && previewAllowed) {
2986
- sendPreview(client, preview);
2987
- }
2988
- });
2989
- }
2990
-
2991
- function schedulePreviewBroadcast(preview) {
2992
- const agentId = preview && preview.agentId;
2993
- if (!agentId) {
2994
- broadcastSessionPreview(preview);
2995
- return;
2996
- }
2997
-
2998
- const now = Date.now();
2999
- const entry = pendingPreviewBroadcasts.get(agentId) || {
3000
- lastAt: 0,
3001
- timer: null,
3002
- preview: null,
3003
- };
3004
- entry.preview = preview;
3005
-
3006
- const elapsed = now - entry.lastAt;
3007
- if (elapsed >= PREVIEW_BROADCAST_INTERVAL_MS) {
3008
- if (entry.timer) {
3009
- clearTimeout(entry.timer);
3010
- entry.timer = null;
3011
- }
3012
- entry.lastAt = now;
3013
- const latest = entry.preview;
3014
- entry.preview = null;
3015
- pendingPreviewBroadcasts.set(agentId, entry);
3016
- broadcastSessionPreview(latest);
3017
- return;
3018
- }
3019
-
3020
- if (!entry.timer) {
3021
- entry.timer = setTimeout(() => {
3022
- entry.timer = null;
3023
- entry.lastAt = Date.now();
3024
- const latest = entry.preview;
3025
- entry.preview = null;
3026
- pendingPreviewBroadcasts.set(agentId, entry);
3027
- if (latest) {
3028
- broadcastSessionPreview(latest);
3029
- }
3030
- }, PREVIEW_BROADCAST_INTERVAL_MS - elapsed);
3031
- }
3032
-
3033
- pendingPreviewBroadcasts.set(agentId, entry);
3034
- }
3035
-
3036
- agentManager.onUpdate(() => {
3037
- scheduleBroadcastState();
3038
- });
3039
-
3040
- agentManager.on('provider-session-updated', () => {
3041
- agentSessionsCache.invalidate();
3042
- scheduleBroadcastState();
3043
- });
3044
-
3045
- function broadcastAgentActivity(activity) {
3046
- const message = JSON.stringify({
3047
- type: 'agent-activity',
3048
- activity,
3049
- });
3050
- wss.clients.forEach((client) => {
3051
- if (client.readyState === WebSocket.OPEN) {
3052
- client.send(message);
3053
- }
3054
- });
3055
- }
3056
-
3057
- function scheduleAgentActivityBroadcast(activity) {
3058
- if (!activity || !activity.agentId) return;
3059
- const agentId = activity.agentId;
3060
- const existing = pendingAgentActivityBroadcasts.get(agentId);
3061
- if (existing) {
3062
- existing.activity = activity;
3063
- return;
3064
- }
3065
- const entry = {
3066
- activity,
3067
- timer: setTimeout(() => {
3068
- pendingAgentActivityBroadcasts.delete(agentId);
3069
- broadcastAgentActivity(entry.activity);
3070
- }, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
3071
- };
3072
- entry.timer.unref?.();
3073
- pendingAgentActivityBroadcasts.set(agentId, entry);
3074
- }
3075
-
3076
- agentManager.onAgentActivity(scheduleAgentActivityBroadcast);
3077
-
3078
- function scheduleAgentUpdate(update) {
3079
- const patch = sanitizeAgentUpdatePatch(update?.patch);
3080
- if (!update?.agentId || !patch) return;
3081
- const existing = pendingAgentUpdates.get(update.agentId);
3082
- if (existing) {
3083
- Object.assign(existing.patch, patch);
3084
- return;
3085
- }
3086
- const entry = {
3087
- patch,
3088
- timer: setTimeout(() => {
3089
- pendingAgentUpdates.delete(update.agentId);
3090
- const message = JSON.stringify({
3091
- type: 'agent-update',
3092
- update: { agentId: update.agentId, patch: entry.patch },
3093
- });
3094
- wss.clients.forEach((client) => {
3095
- if (client.readyState === WebSocket.OPEN) client.send(message);
3096
- });
3097
- }, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
3098
- };
3099
- entry.timer.unref?.();
3100
- pendingAgentUpdates.set(update.agentId, entry);
3101
- }
3102
-
3103
- agentManager.on('agent-update', scheduleAgentUpdate);
3104
-
3105
- function broadcastAgentRead(read) {
3106
- if (!read || !read.agentId) return;
3107
- const message = JSON.stringify({ type: 'agent-read', read });
3108
- wss.clients.forEach((client) => {
3109
- if (client.readyState === WebSocket.OPEN) {
3110
- client.send(message);
3111
- }
3112
- });
3113
- }
3114
-
3115
- agentManager.on('agent-read', broadcastAgentRead);
3116
-
3117
- function sessionStreamKey(stream) {
3118
- return `${stream.agentId || ''}\0${stream.sessionSource || ''}`;
3119
- }
3120
-
3121
- function broadcastSessionStream(stream) {
3122
- const message = JSON.stringify({
3123
- type: 'session-output',
3124
- stream
3125
- });
3126
- deliverSessionStreamToClients(wss.clients, stream, {
3127
- openState: WebSocket.OPEN,
3128
- maxBufferedAmount: MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT,
3129
- message,
3130
- });
3131
- }
3132
-
3133
- function flushSessionStreams() {
3134
- sessionStreamBroadcastTimer = null;
3135
- const streams = Array.from(pendingSessionStreams.values());
3136
- pendingSessionStreams.clear();
3137
- if (streams.length > 0) lastSessionStreamBroadcastAt = Date.now();
3138
- streams.forEach(broadcastSessionStream);
3139
- }
3140
-
3141
- function scheduleSessionStreamBroadcast(stream) {
3142
- if (!stream || !stream.agentId) return;
3143
- const now = Date.now();
3144
- if (shouldBroadcastSessionStreamImmediately({
3145
- pendingCount: pendingSessionStreams.size,
3146
- lastBroadcastAt: lastSessionStreamBroadcastAt,
3147
- now,
3148
- intervalMs: SESSION_STREAM_BROADCAST_INTERVAL_MS,
3149
- })) {
3150
- lastSessionStreamBroadcastAt = now;
3151
- broadcastSessionStream(coalesceSessionStream(null, stream));
3152
- return;
3153
- }
3154
- const key = sessionStreamKey(stream);
3155
- const existing = pendingSessionStreams.get(key);
3156
- pendingSessionStreams.set(key, coalesceSessionStream(existing, stream));
3157
-
3158
- if (!sessionStreamBroadcastTimer) {
3159
- sessionStreamBroadcastTimer = setTimeout(flushSessionStreams, SESSION_STREAM_BROADCAST_INTERVAL_MS);
3160
- if (typeof sessionStreamBroadcastTimer.unref === 'function') sessionStreamBroadcastTimer.unref();
3161
- }
3162
- }
3163
-
3164
- agentManager.onSessionStream((stream) => {
3165
- scheduleSessionStreamBroadcast(stream);
3166
- });
3167
-
3168
- agentManager.onSessionPreview((preview) => {
3169
- schedulePreviewBroadcast(preview);
3170
- });
3171
-
3172
- agentManager.onSystemStats((systemStats) => {
3173
- const usageSnapshot = agentManager.getAgentUsageSnapshots();
3174
- const message = JSON.stringify({
3175
- type: 'system-stats',
3176
- stats: {
3177
- ...systemStats,
3178
- ip: getPrimaryLocalIP(),
3179
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
3180
- },
3181
- uptime: agentManager.getUptime(),
3182
- usageRate: {
3183
- windowMs: usageSnapshot.windowMs,
3184
- estimatedTokensPerMinute: usageSnapshot.estimatedTokensPerMinute,
3185
- source: usageSnapshot.source,
3186
- }
3187
- });
3188
- wss.clients.forEach(client => {
3189
- if (client.readyState === WebSocket.OPEN) {
3190
- client.send(message);
3191
- }
3192
- });
3193
- });
3194
-
3195
- let serverStarted = false;
3196
- let terminalRuntimeCleanupPromise = null;
3197
-
3198
- function runTerminalRuntimeStartupCleanup() {
3199
- if (!terminalRuntimeCleanupPromise) {
3200
- terminalRuntimeCleanupPromise = cleanupTerminalRuntime({ configDir: configManager.farmingDir })
3201
- .catch(error => {
3202
- console.warn('Failed to clean terminal runtime leftovers:', error && (error.message || error));
3203
- return null;
3204
- });
3205
- }
3206
- return terminalRuntimeCleanupPromise;
3207
- }
3208
-
3209
- function startServer() {
3210
- if (serverStarted) return server;
3211
- serverStarted = true;
3212
-
3213
- void Promise.all([
3214
- runTerminalRuntimeStartupCleanup(),
3215
- browserRuntimeRecoveryPromise,
3216
- ]).finally(() => {
3217
- server.listen(PORT, () => {
3218
- const token = tokenAuth.getToken();
3219
- const localIPs = getLocalIPs();
3220
- const entryPath = BASE_PATH || '/';
3221
- const entrySuffix = authEnabled ? `${entryPath}?token=${token}` : entryPath;
3222
-
3223
- console.log('');
3224
- console.log(' Farming server running on:');
3225
- console.log('');
3226
- console.log(` Local: http://localhost:${PORT}${entrySuffix}`);
3227
- localIPs.forEach(ip => {
3228
- console.log(` Network: http://${ip}:${PORT}${entrySuffix}`);
3229
- });
3230
- console.log('');
3231
- if (authEnabled) {
3232
- console.log(` Token: ${token}`);
3233
- const tokenInfo = tokenAuth.getTokenInfo();
3234
- if (tokenInfo) {
3235
- console.log(` Token style: ${tokenInfo.style} (${tokenInfo.source}, ~${tokenInfo.entropyBits} bits)`);
3236
- }
3237
- } else {
3238
- console.log(' Token auth: disabled');
3239
- }
3240
- console.log('');
3241
- setTimeout(warmCodexExecutableVersionCache, 100);
3242
- void autoResumeMainPageAgentSessions();
3243
- });
3244
- });
3245
-
3246
- return server;
3247
- }
3248
-
3249
- if (require.main === module) {
3250
- startServer();
3251
- }
3252
-
3253
- module.exports = {
3254
- app,
3255
- server,
3256
- wss,
3257
- agentManager,
3258
- browserResourceManager,
3259
- workspaceFileService,
3260
- handleMessage,
3261
- resolveCliBinDir,
3262
- resolveInputTargetAgentId,
3263
- rewriteIndexHtmlForBasePath,
3264
- appendIndexHtmlAssetToken,
3265
- startServer,
3266
- runTerminalRuntimeStartupCleanup,
3267
- };