@vantran-se/openchamber-web 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -1,6 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test, vi } from 'bun:test';
2
+ import { execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
2
6
 
3
- import { findBranchPrCandidates, invalidateRepoPullsCache, isHistoricalPrOfCheckout } from './pr-status.js';
7
+ import { findBranchPrCandidates, invalidateRepoPullsCache, isHistoricalPrOfCheckout, resolveGitHubPrStatus } from './pr-status.js';
4
8
  import { createOctokit, getOctokitCacheIdentity } from './octokit.js';
5
9
 
6
10
  const listMock = mock(async () => ({ data: [] }));
@@ -274,6 +278,110 @@ describe('findBranchPrCandidates', () => {
274
278
  expect(listMock.mock.calls.some((entry) => entry[0]?.state === 'all')).toBe(true);
275
279
  expect(listMock.mock.calls.length).toBeGreaterThan(callsAfterFirst + 1);
276
280
  });
281
+
282
+ test('does not reuse a cached history miss for a different source owner', async () => {
283
+ const target = { repo: { owner: 'upstream-cache-scope', repo: 'app' }, remoteName: 'origin' };
284
+ const ownerA = { repo: { owner: 'fork-cache-a', repo: 'app' } };
285
+ const ownerB = { repo: { owner: 'fork-cache-b', repo: 'app' } };
286
+ const ownerBPr = {
287
+ ...mergedPr,
288
+ number: 16,
289
+ head: { ...mergedPr.head, label: 'fork-cache-b:feature', user: { login: 'fork-cache-b' }, repo: { owner: { login: 'fork-cache-b' }, name: 'app' } },
290
+ };
291
+ listMock.mockImplementation(async ({ head }) => (
292
+ head === 'fork-cache-b:feature' ? { data: [ownerBPr] } : { data: [] }
293
+ ));
294
+ const octokit = octokitFor('test-token', 'test-account');
295
+
296
+ await findBranchPrCandidates({
297
+ octokit,
298
+ target,
299
+ branch: 'feature',
300
+ sourceCandidates: [ownerA],
301
+ force: true,
302
+ includeHistory: true,
303
+ });
304
+ const { historical } = await findBranchPrCandidates({
305
+ octokit,
306
+ target,
307
+ branch: 'feature',
308
+ sourceCandidates: [ownerB],
309
+ includeHistory: true,
310
+ });
311
+
312
+ expect(historical?.number).toBe(16);
313
+ });
314
+ });
315
+
316
+ describe('resolveGitHubPrStatus for a contributor fork checkout', () => {
317
+ let directory;
318
+
319
+ beforeEach(async () => {
320
+ directory = await fs.mkdtemp(path.join(os.tmpdir(), 'pr-status-fork-'));
321
+ const git = (...args) => execFileSync('git', args, { cwd: directory, stdio: 'ignore' });
322
+ git('init', '-q', '-b', 'fix/thing');
323
+ git('remote', 'add', 'origin', 'https://github.com/acme/app.git');
324
+ git('remote', 'add', 'pr-contrib', 'https://github.com/contrib/app.git');
325
+ invalidateRepoPullsCache('acme', 'app');
326
+ });
327
+
328
+ afterEach(async () => {
329
+ await fs.rm(directory, { recursive: true, force: true });
330
+ });
331
+
332
+ const forkPr = {
333
+ number: 44,
334
+ state: 'open',
335
+ head: { ref: 'fix/thing', label: 'contrib:fix/thing', user: { login: 'contrib' }, repo: { owner: { login: 'contrib' }, name: 'app' } },
336
+ };
337
+ const octokit = () => ({
338
+ openChamberCacheIdentity: getOctokitCacheIdentity(createOctokit('fork-token', 'fork-account')),
339
+ rest: {
340
+ repos: {
341
+ get: async ({ owner, repo }) => ({
342
+ data: owner === 'contrib'
343
+ ? { default_branch: 'main', parent: { owner: { login: 'acme' }, name: repo } }
344
+ : { default_branch: 'main' },
345
+ }),
346
+ },
347
+ pulls: { list: async ({ owner }) => ({ data: owner === 'acme' ? [forkPr] : [] }) },
348
+ },
349
+ });
350
+
351
+ test('finds the fork PR when the caller names the fork remote as the branch source', async () => {
352
+ const status = await resolveGitHubPrStatus({
353
+ octokit: octokit(), directory, branch: 'fix/thing', remoteName: 'origin', sourceRemoteName: 'pr-contrib', force: true,
354
+ });
355
+ expect(status.pr?.number).toBe(44);
356
+ expect(status.repo).toMatchObject({ owner: 'acme', repo: 'app' });
357
+ });
358
+
359
+ test('finds the merged fork PR in the primary remote once it is merged', async () => {
360
+ git(directory, '-c', 'user.name=Test User', '-c', 'user.email=test@example.com', 'commit', '-q', '--allow-empty', '-m', 'fix');
361
+ const mergedForkPr = {
362
+ ...forkPr,
363
+ state: 'closed',
364
+ merged_at: '2026-01-01T00:00:00Z',
365
+ head: { ...forkPr.head, sha: git(directory, 'rev-parse', 'HEAD').trim() },
366
+ };
367
+ const client = octokit();
368
+ client.rest.pulls.list = async ({ owner, state, head }) => ({
369
+ data: owner === 'acme' && state === 'all' && head === 'contrib:fix/thing' ? [mergedForkPr] : [],
370
+ });
371
+
372
+ const status = await resolveGitHubPrStatus({
373
+ octokit: client, directory, branch: 'fix/thing', remoteName: 'origin', sourceRemoteName: 'pr-contrib', force: true,
374
+ });
375
+ expect(status.pr?.number).toBe(44);
376
+ expect(status.repo).toMatchObject({ owner: 'acme', repo: 'app' });
377
+ });
378
+
379
+ test('without a source remote a same-named fork branch is not this branch', async () => {
380
+ const status = await resolveGitHubPrStatus({
381
+ octokit: octokit(), directory, branch: 'fix/thing', remoteName: 'origin', force: true,
382
+ });
383
+ expect(status.pr).toBeNull();
384
+ });
277
385
  });
278
386
 
279
387
  describe('isHistoricalPrOfCheckout', () => {
@@ -283,15 +391,30 @@ describe('isHistoricalPrOfCheckout', () => {
283
391
 
284
392
  test('a merged PR whose head commit is in the checkout history belongs to it', async () => {
285
393
  isAncestorMock.mockImplementation(async () => true);
394
+ const isPatchEquivalent = mock(async () => false);
286
395
  const pr = { ...mergedPr, head: { ...mergedPr.head, sha: 'abc1234' } };
287
- expect(await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock })).toBe(true);
396
+ expect(await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock, isPatchEquivalent })).toBe(true);
288
397
  expect(isAncestorMock).toHaveBeenCalledWith('/repo', 'abc1234');
398
+ expect(isPatchEquivalent).not.toHaveBeenCalled();
399
+ });
400
+
401
+ test('a PR with a patch-equivalent head commit belongs to the checkout', async () => {
402
+ isAncestorMock.mockImplementation(async () => false);
403
+ const isPatchEquivalent = mock(async () => true);
404
+ const pr = { ...mergedPr, head: { ...mergedPr.head, sha: 'abc1234' } };
405
+
406
+ const belongsToCheckout = await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock, isPatchEquivalent });
407
+
408
+ expect(isPatchEquivalent).toHaveBeenCalledWith('/repo', 'abc1234');
409
+ expect(belongsToCheckout).toBe(true);
289
410
  });
290
411
 
291
412
  test('a reused branch name without the merged commits does not inherit the PR', async () => {
292
413
  isAncestorMock.mockImplementation(async () => false);
414
+ const isPatchEquivalent = mock(async () => false);
293
415
  const pr = { ...mergedPr, head: { ...mergedPr.head, sha: 'abc1234' } };
294
- expect(await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock })).toBe(false);
416
+ expect(await isHistoricalPrOfCheckout('/repo', pr, { isAncestor: isAncestorMock, isPatchEquivalent })).toBe(false);
417
+ expect(isPatchEquivalent).toHaveBeenCalledWith('/repo', 'abc1234');
295
418
  });
296
419
 
297
420
  test('a PR without a head sha is never attributed', async () => {
@@ -299,3 +422,148 @@ describe('isHistoricalPrOfCheckout', () => {
299
422
  expect(isAncestorMock).not.toHaveBeenCalled();
300
423
  });
301
424
  });
425
+
426
+ const git = (directory, ...args) => execFileSync('git', ['-C', directory, ...args], {
427
+ encoding: 'utf8',
428
+ });
429
+
430
+ const createRebasedCheckout = async (suffix, branch = 'feature') => {
431
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), `oc-pr-history-${suffix}-`));
432
+ git(directory, 'init', `--initial-branch=${branch}`);
433
+ git(directory, 'config', 'user.name', 'Test User');
434
+ git(directory, 'config', 'user.email', 'test@example.com');
435
+ await fs.writeFile(path.join(directory, 'status.txt'), 'base\n');
436
+ git(directory, 'add', 'status.txt');
437
+ git(directory, 'commit', '-m', 'base');
438
+ await fs.writeFile(path.join(directory, 'status.txt'), 'rebased change\n');
439
+ git(directory, 'commit', '-am', 'original change');
440
+ const originalSha = git(directory, 'rev-parse', 'HEAD').trim();
441
+ git(directory, 'reset', '--hard', 'HEAD~1');
442
+ await fs.writeFile(path.join(directory, 'status.txt'), 'rebased change\n');
443
+ git(directory, 'commit', '-am', 'rebased change');
444
+ const baseSha = git(directory, 'rev-parse', 'HEAD~1').trim();
445
+ git(directory, 'remote', 'add', 'origin', `https://github.com/fork-${suffix}/app-${suffix}.git`);
446
+ git(directory, 'remote', 'add', 'upstream', `https://github.com/upstream-${suffix}/app-${suffix}.git`);
447
+ git(directory, 'remote', 'add', 'contributor', `https://github.com/contributor-${suffix}/app-${suffix}.git`);
448
+ return { directory, originalSha, baseSha };
449
+ };
450
+
451
+ const createForkNetworkOctokit = ({ suffix, historicalPr, forkHistoricalPr = null, openParentPr = null, branch = 'feature' }) => {
452
+ const calls = [];
453
+ const fork = { owner: `fork-${suffix}`, repo: `app-${suffix}` };
454
+ const parent = { owner: `upstream-${suffix}`, repo: `app-${suffix}` };
455
+ const contributor = { owner: `contributor-${suffix}`, repo: `app-${suffix}` };
456
+ const octokit = {
457
+ openChamberCacheIdentity: getOctokitCacheIdentity(createOctokit(`token-${suffix}`, `account-${suffix}`)),
458
+ rest: {
459
+ repos: {
460
+ get: async ({ owner, repo }) => {
461
+ if (owner === fork.owner && repo === fork.repo) {
462
+ return { data: { default_branch: 'main', parent: { owner: { login: parent.owner }, name: parent.repo } } };
463
+ }
464
+ return { data: { default_branch: 'main' } };
465
+ },
466
+ },
467
+ pulls: {
468
+ list: async (options) => {
469
+ calls.push(options);
470
+ if (!options.head && options.owner === parent.owner && openParentPr) {
471
+ return { data: [openParentPr] };
472
+ }
473
+ if (options.owner === fork.owner && options.state === 'all' && options.head === `${fork.owner}:${branch}` && forkHistoricalPr) {
474
+ return { data: [forkHistoricalPr] };
475
+ }
476
+ if (options.owner === parent.owner && options.state === 'all' && options.head === `${fork.owner}:${branch}`) {
477
+ return { data: [historicalPr] };
478
+ }
479
+ return { data: [] };
480
+ },
481
+ },
482
+ },
483
+ };
484
+ return { octokit, calls, fork, parent, contributor };
485
+ };
486
+
487
+ describe('resolveGitHubPrStatus fork history', () => {
488
+ test.each([
489
+ ['closed', null],
490
+ ['merged', '2026-01-01T00:00:00Z'],
491
+ ])('restores a patch-equivalent %s PR from the primary fork parent', async (state, mergedAt) => {
492
+ const suffix = `terminal-${state}`;
493
+ const { directory, originalSha } = await createRebasedCheckout(suffix);
494
+ const historicalPr = {
495
+ number: state === 'closed' ? 301 : 302,
496
+ state: 'closed',
497
+ merged_at: mergedAt,
498
+ head: {
499
+ sha: originalSha,
500
+ ref: 'feature',
501
+ label: `fork-${suffix}:feature`,
502
+ user: { login: `fork-${suffix}` },
503
+ repo: { owner: { login: `fork-${suffix}` }, name: `app-${suffix}` },
504
+ },
505
+ };
506
+ const { octokit, calls, fork, parent, contributor } = createForkNetworkOctokit({ suffix, historicalPr });
507
+
508
+ try {
509
+ const resolved = await resolveGitHubPrStatus({ octokit, directory, branch: 'feature', remoteName: 'origin', force: true });
510
+
511
+ expect(resolved.pr?.number).toBe(historicalPr.number);
512
+ expect(resolved.repo).toMatchObject(parent);
513
+ const historyCalls = calls.filter((call) => call.state === 'all');
514
+ expect(historyCalls).toEqual([
515
+ expect.objectContaining({ owner: fork.owner, head: `${fork.owner}:feature` }),
516
+ expect.objectContaining({ owner: fork.owner, head: `${parent.owner}:feature` }),
517
+ expect.objectContaining({ owner: parent.owner, head: `${fork.owner}:feature` }),
518
+ expect.objectContaining({ owner: parent.owner, head: `${parent.owner}:feature` }),
519
+ ]);
520
+ expect(historyCalls.some((call) => call.owner === contributor.owner)).toBe(false);
521
+
522
+ const callsAfterFirst = calls.length;
523
+ const repeated = await resolveGitHubPrStatus({ octokit, directory, branch: 'feature', remoteName: 'origin' });
524
+ expect(repeated.pr?.number).toBe(historicalPr.number);
525
+ expect(calls).toHaveLength(callsAfterFirst);
526
+ } finally {
527
+ await fs.rm(directory, { recursive: true, force: true });
528
+ }
529
+ });
530
+
531
+ test('returns an open parent PR before an earlier terminal fork PR', async () => {
532
+ const suffix = 'open-priority';
533
+ const { directory, originalSha } = await createRebasedCheckout(suffix);
534
+ const terminalPr = {
535
+ number: 303,
536
+ state: 'closed',
537
+ head: { sha: originalSha, ref: 'feature', label: `fork-${suffix}:feature`, user: { login: `fork-${suffix}` }, repo: { owner: { login: `fork-${suffix}` }, name: `app-${suffix}` } },
538
+ };
539
+ const openParentPr = { ...terminalPr, number: 304, state: 'open', merged_at: null };
540
+ const { octokit } = createForkNetworkOctokit({ suffix, historicalPr: terminalPr, forkHistoricalPr: terminalPr, openParentPr });
541
+
542
+ try {
543
+ const resolved = await resolveGitHubPrStatus({ octokit, directory, branch: 'feature', remoteName: 'origin', force: true });
544
+ expect(resolved.pr?.number).toBe(openParentPr.number);
545
+ } finally {
546
+ await fs.rm(directory, { recursive: true, force: true });
547
+ }
548
+ });
549
+
550
+ test('does not inherit terminal fork history on the default branch', async () => {
551
+ const suffix = 'default-main-history';
552
+ const { directory, baseSha } = await createRebasedCheckout(suffix, 'main');
553
+ const terminalPr = {
554
+ number: 305,
555
+ state: 'closed',
556
+ merged_at: '2026-01-01T00:00:00Z',
557
+ head: { sha: baseSha, ref: 'main', label: `fork-${suffix}:main`, user: { login: `fork-${suffix}` }, repo: { owner: { login: `fork-${suffix}` }, name: `app-${suffix}` } },
558
+ };
559
+ const { octokit, calls } = createForkNetworkOctokit({ suffix, historicalPr: terminalPr, branch: 'main' });
560
+
561
+ try {
562
+ const resolved = await resolveGitHubPrStatus({ octokit, directory, branch: 'main', remoteName: 'origin', force: true });
563
+ expect(resolved.pr).toBeNull();
564
+ expect(calls.some((call) => call.state === 'all')).toBe(false);
565
+ } finally {
566
+ await fs.rm(directory, { recursive: true, force: true });
567
+ }
568
+ });
569
+ });
@@ -22,14 +22,21 @@
22
22
 
23
23
  import { z } from 'zod';
24
24
 
25
- import { isGraphqlRateLimitError, parseSummaryRefs } from './pr-summaries.js';
25
+ import { checkContextSchema, isGraphqlRateLimitError, parseSummaryRefs, summarizeCheckContexts } from './pr-summaries.js';
26
26
 
27
27
  const REFERENCE_PAGE_SIZE = 30;
28
28
  // The preview shows the description; the attach path reads the full one.
29
29
  const BODY_PREVIEW_MAX = 20_000;
30
30
  // Keys match the filter schema below.
31
- const SEARCH_FILTERS = {
32
- open: '',
31
+ // The list's state and whose items it shows combine, one qualifier each.
32
+ const SEARCH_STATES = {
33
+ open: 'is:open',
34
+ closed: 'is:closed',
35
+ merged: 'is:merged',
36
+ all: '',
37
+ };
38
+ const SEARCH_PEOPLE = {
39
+ any: '',
33
40
  assigned: 'assignee:@me',
34
41
  created: 'author:@me',
35
42
  reviewRequested: 'review-requested:@me',
@@ -67,6 +74,8 @@ fragment PullReference on PullRequest {
67
74
 
68
75
  // The newest comments and reviews; `totalCount` says how many there are.
69
76
  const DETAIL_COMMENT_LIMIT = 50;
77
+ // The newest commits, so the preview can show what each review answered.
78
+ const DETAIL_COMMIT_LIMIT = 50;
70
79
 
71
80
  const DETAIL_QUERY = `
72
81
  query ReferenceDetail($owner: String!, $repo: String!, $number: Int!) {
@@ -79,11 +88,33 @@ query ReferenceDetail($owner: String!, $repo: String!, $number: Int!) {
79
88
  }
80
89
  ... on PullRequest {
81
90
  number
91
+ state
82
92
  reviewDecision
93
+ # The preview's checks, from the PR itself rather than the list's batch.
94
+ headCommit: commits(last: 1) {
95
+ nodes {
96
+ commit {
97
+ statusCheckRollup {
98
+ contexts(first: 100) {
99
+ nodes {
100
+ __typename
101
+ ... on CheckRun { databaseId name status conclusion startedAt checkSuite { app { databaseId } } }
102
+ ... on StatusContext { context state }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ reviewRequests(first: 30) { nodes { requestedReviewer { __typename ... on User { login avatarUrl } } } }
83
110
  additions
84
111
  deletions
85
112
  changedFiles
86
113
  comments(last: ${DETAIL_COMMENT_LIMIT}) { totalCount nodes { ...DetailComment } }
114
+ commits(last: ${DETAIL_COMMIT_LIMIT}) {
115
+ totalCount
116
+ nodes { commit { oid messageHeadline committedDate url author { name user { login avatarUrl } } } }
117
+ }
87
118
  reviews(last: 30) {
88
119
  nodes {
89
120
  author { login avatarUrl }
@@ -175,11 +206,40 @@ const detailSchema = z.object({
175
206
  z.object({
176
207
  __typename: z.literal('PullRequest'),
177
208
  number: z.number().int(),
209
+ state: z.enum(['OPEN', 'CLOSED', 'MERGED']),
178
210
  reviewDecision: z.enum(['APPROVED', 'CHANGES_REQUESTED', 'REVIEW_REQUIRED']).nullable(),
211
+ headCommit: z.object({
212
+ nodes: z.array(z.object({
213
+ commit: z.object({
214
+ statusCheckRollup: z.object({ contexts: z.object({ nodes: z.array(checkContextSchema) }) }).nullable(),
215
+ }),
216
+ })),
217
+ }),
218
+ reviewRequests: z.object({
219
+ nodes: z.array(z.object({
220
+ requestedReviewer: z.object({
221
+ __typename: z.string(),
222
+ login: z.string().optional(),
223
+ avatarUrl: z.string().nullable().optional(),
224
+ }).nullable(),
225
+ })),
226
+ }),
179
227
  additions: z.number().int(),
180
228
  deletions: z.number().int(),
181
229
  changedFiles: z.number().int(),
182
230
  comments: z.object({ totalCount: z.number().int(), nodes: z.array(detailCommentSchema) }),
231
+ commits: z.object({
232
+ totalCount: z.number().int(),
233
+ nodes: z.array(z.object({
234
+ commit: z.object({
235
+ oid: z.string(),
236
+ messageHeadline: z.string(),
237
+ committedDate: z.string().nullable(),
238
+ url: z.string().nullable(),
239
+ author: z.object({ name: z.string().nullable(), user: userSchema }).nullable(),
240
+ }),
241
+ })),
242
+ }),
183
243
  reviews: z.object({
184
244
  nodes: z.array(z.object({
185
245
  author: userSchema,
@@ -322,9 +382,29 @@ export async function fetchReferenceDetail({ octokit, owner, repo, number }) {
322
382
  commentTotal: item.comments.totalCount,
323
383
  pull: {
324
384
  reviewDecision: item.reviewDecision ? REVIEW_DECISIONS[item.reviewDecision] : null,
385
+ // People asked to review and not done yet; a team request is left out
386
+ // since the board only picks people.
387
+ reviewers: item.reviewRequests.nodes.flatMap(({ requestedReviewer: reviewer }) => (
388
+ reviewer?.__typename === 'User' && reviewer.login
389
+ ? [{ id: reviewer.login, ...toAuthor({ login: reviewer.login, avatarUrl: reviewer.avatarUrl }) }]
390
+ : []
391
+ )),
392
+ // Like the list's statuses: a closed or merged PR's checks are not actionable.
393
+ checks: item.state === 'OPEN'
394
+ ? summarizeCheckContexts(item.headCommit.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [])
395
+ : null,
325
396
  additions: item.additions,
326
397
  deletions: item.deletions,
327
398
  changedFiles: item.changedFiles,
399
+ commits: item.commits.nodes.map(({ commit }) => ({
400
+ sha: commit.oid,
401
+ headline: commit.messageHeadline,
402
+ author: toAuthor(commit.author?.user ?? null),
403
+ authorName: commit.author?.name ?? null,
404
+ committedAt: commit.committedDate,
405
+ url: commit.url,
406
+ })),
407
+ commitTotal: item.commits.totalCount,
328
408
  },
329
409
  };
330
410
  }
@@ -348,12 +428,18 @@ export function readPullStatusRefs(value) {
348
428
  return parseSummaryRefs(refs);
349
429
  }
350
430
 
351
- const filterSchema = z.enum(['open', 'assigned', 'created', 'reviewRequested']).catch('open');
431
+ const stateSchema = z.enum(['open', 'closed', 'merged', 'all']).catch('open');
432
+ const peopleSchema = z.enum(['any', 'assigned', 'created', 'reviewRequested']).catch('any');
352
433
  const kindSchema = z.enum(['issue', 'pull']).nullable().catch(null);
353
434
 
354
- /** The picker's filter values; anything else reads as `open`. */
355
- export function readReferenceFilter(value) {
356
- return filterSchema.parse(value);
435
+ /** A list's state; anything else reads as `open`. */
436
+ export function readReferenceState(value) {
437
+ return stateSchema.parse(value);
438
+ }
439
+
440
+ /** Whose items a list shows; anything else reads as `any`. */
441
+ export function readReferencePeople(value) {
442
+ return peopleSchema.parse(value);
357
443
  }
358
444
 
359
445
  /** `issue` or `pull`; anything else is null. */
@@ -391,18 +477,21 @@ export function parseReferenceLookup(text, repos) {
391
477
  * The GitHub search string for one picker page. The user's text may carry
392
478
  * its own qualifiers; state and sort are added only when it does not.
393
479
  */
394
- export function buildReferenceSearchQuery({ repos, kind, filter, text }) {
480
+ export function buildReferenceSearchQuery({ repos, kind, state, people, text }) {
395
481
  const userText = text.trim();
396
482
  const parts = repos.map((entry) => `repo:${entry.owner}/${entry.repo}`);
397
483
  parts.push(kind === 'pull' ? 'is:pr' : 'is:issue');
398
484
  if (!/(^|\s)(is:(open|closed|merged|unmerged)|state:\S+)/i.test(userText)) {
399
- parts.push('is:open');
485
+ // An issue is never merged; the nearest list is its closed one.
486
+ const stateQualifier = SEARCH_STATES[kind === 'issue' && state === 'merged' ? 'closed' : state];
487
+ if (stateQualifier) parts.push(stateQualifier);
400
488
  }
401
489
  if (!/(^|\s)sort:\S+/i.test(userText)) {
402
490
  parts.push('sort:updated-desc');
403
491
  }
404
- const qualifier = SEARCH_FILTERS[filter];
405
- if (qualifier) parts.push(qualifier);
492
+ // Review requests exist for pull requests only.
493
+ const peopleQualifier = kind === 'issue' && people === 'reviewRequested' ? '' : SEARCH_PEOPLE[people];
494
+ if (peopleQualifier) parts.push(peopleQualifier);
406
495
  if (userText) parts.push(userText);
407
496
  return parts.join(' ');
408
497
  }
@@ -442,7 +531,7 @@ async function runPartialQuery(octokit, query, variables) {
442
531
  * first. A lookup answers every repo that has the number, both kinds; a
443
532
  * search answers one kind, newest activity first.
444
533
  */
445
- export async function searchGitHubReferences({ octokit, repos, kind, filter, text, cursor }) {
534
+ export async function searchGitHubReferences({ octokit, repos, kind, state, people, text, cursor }) {
446
535
  const lookup = parseReferenceLookup(text, repos);
447
536
  if (lookup) {
448
537
  if (lookup.repos.length === 0) {
@@ -457,7 +546,7 @@ export async function searchGitHubReferences({ octokit, repos, kind, filter, tex
457
546
  }
458
547
 
459
548
  const { search } = searchResultSchema.parse(await octokit.graphql(SEARCH_QUERY, {
460
- q: buildReferenceSearchQuery({ repos, kind, filter, text }),
549
+ q: buildReferenceSearchQuery({ repos, kind, state, people, text }),
461
550
  first: REFERENCE_PAGE_SIZE,
462
551
  after: cursor || null,
463
552
  }));
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { buildReferenceSearchQuery, fetchReferenceDetail, parseReferenceLookup, readReferenceFilter, toReference } from './reference-search.js';
3
+ import { buildReferenceSearchQuery, fetchReferenceDetail, parseReferenceLookup, readReferencePeople, readReferenceState, toReference } from './reference-search.js';
4
4
 
5
5
  const origin = { owner: 'acme', repo: 'app', source: 'origin' };
6
6
  const upstream = { owner: 'Upstream', repo: 'App', source: 'upstream' };
@@ -21,20 +21,36 @@ const common = (number, repository = { name: 'app', owner: { login: 'acme' } })
21
21
 
22
22
  describe('buildReferenceSearchQuery', () => {
23
23
  it('lists open items of one kind across the network, newest activity first', () => {
24
- expect(buildReferenceSearchQuery({ repos, kind: 'issue', filter: 'open', text: '' }))
24
+ expect(buildReferenceSearchQuery({ repos, kind: 'issue', state: 'open', people: 'any', text: '' }))
25
25
  .toBe('repo:acme/app repo:Upstream/App is:issue is:open sort:updated-desc');
26
- expect(buildReferenceSearchQuery({ repos: [origin], kind: 'pull', filter: 'reviewRequested', text: ' crash ' }))
26
+ expect(buildReferenceSearchQuery({ repos: [origin], kind: 'pull', state: 'open', people: 'reviewRequested', text: ' crash ' }))
27
27
  .toBe('repo:acme/app is:pr is:open sort:updated-desc review-requested:@me crash');
28
28
  });
29
29
 
30
+ it('combines a state with whose items they are', () => {
31
+ expect(buildReferenceSearchQuery({ repos: [origin], kind: 'pull', state: 'closed', people: 'created', text: '' }))
32
+ .toBe('repo:acme/app is:pr is:closed sort:updated-desc author:@me');
33
+ expect(buildReferenceSearchQuery({ repos: [origin], kind: 'pull', state: 'merged', people: 'assigned', text: '' }))
34
+ .toBe('repo:acme/app is:pr is:merged sort:updated-desc assignee:@me');
35
+ expect(buildReferenceSearchQuery({ repos: [origin], kind: 'issue', state: 'all', people: 'any', text: '' }))
36
+ .toBe('repo:acme/app is:issue sort:updated-desc');
37
+ });
38
+
39
+ it('reads issue-only nonsense as the nearest list: merged as closed, review requests as anyone', () => {
40
+ expect(buildReferenceSearchQuery({ repos: [origin], kind: 'issue', state: 'merged', people: 'reviewRequested', text: '' }))
41
+ .toBe('repo:acme/app is:issue is:closed sort:updated-desc');
42
+ });
43
+
30
44
  it('leaves state and sort to the user when their text sets them', () => {
31
- expect(buildReferenceSearchQuery({ repos: [origin], kind: 'issue', filter: 'assigned', text: 'is:closed sort:created-asc' }))
45
+ expect(buildReferenceSearchQuery({ repos: [origin], kind: 'issue', state: 'open', people: 'assigned', text: 'is:closed sort:created-asc' }))
32
46
  .toBe('repo:acme/app is:issue assignee:@me is:closed sort:created-asc');
33
47
  });
34
48
 
35
- it('reads an unknown filter as open', () => {
36
- expect(readReferenceFilter('everything')).toBe('open');
37
- expect(readReferenceFilter('created')).toBe('created');
49
+ it('reads unknown values as open and anyone', () => {
50
+ expect(readReferenceState('everything')).toBe('open');
51
+ expect(readReferenceState('merged')).toBe('merged');
52
+ expect(readReferencePeople('everyone')).toBe('any');
53
+ expect(readReferencePeople('created')).toBe('created');
38
54
  });
39
55
  });
40
56
 
@@ -130,11 +146,25 @@ describe('fetchReferenceDetail', () => {
130
146
  const pull = (reviews = []) => ({
131
147
  __typename: 'PullRequest',
132
148
  number: 9,
149
+ state: 'OPEN',
133
150
  reviewDecision: 'CHANGES_REQUESTED',
151
+ headCommit: { nodes: [{ commit: { statusCheckRollup: { contexts: { nodes: [
152
+ { __typename: 'CheckRun', databaseId: 1, name: 'test', status: 'COMPLETED', conclusion: 'FAILURE', startedAt: null, checkSuite: { app: { databaseId: 15368 } } },
153
+ { __typename: 'CheckRun', databaseId: 2, name: 'lint', status: 'COMPLETED', conclusion: 'SUCCESS', startedAt: null, checkSuite: { app: { databaseId: 15368 } } },
154
+ ] } } } }] },
155
+ reviewRequests: { nodes: [
156
+ { requestedReviewer: { __typename: 'User', login: 'hubot', avatarUrl: null } },
157
+ { requestedReviewer: { __typename: 'Team' } },
158
+ { requestedReviewer: null },
159
+ ] },
134
160
  additions: 12,
135
161
  deletions: 3,
136
162
  changedFiles: 2,
137
163
  comments: { totalCount: 1, nodes: [{ author, body: 'first', createdAt: '2026-10-01T10:00:00Z', url: 'u1' }] },
164
+ commits: { totalCount: 1, nodes: [{ commit: {
165
+ oid: 'abc1234', messageHeadline: 'fix: the thing', committedDate: '2026-10-01T08:00:00Z', url: 'https://github.com/acme/app/commit/abc1234',
166
+ author: { name: 'Octo Cat', user: author },
167
+ } }] },
138
168
  reviews: { nodes: reviews },
139
169
  });
140
170
 
@@ -166,9 +196,20 @@ describe('fetchReferenceDetail', () => {
166
196
  ]);
167
197
  expect(detail?.pull).toEqual({
168
198
  reviewDecision: 'changes_requested',
199
+ reviewers: [{ id: 'hubot', login: 'hubot', avatarUrl: undefined }],
200
+ checks: expect.objectContaining({ state: 'failure', total: 2, success: 1, failure: 1 }),
169
201
  additions: 12,
170
202
  deletions: 3,
171
203
  changedFiles: 2,
204
+ commits: [{
205
+ sha: 'abc1234',
206
+ headline: 'fix: the thing',
207
+ author: { login: 'octo', avatarUrl: 'https://avatars/octo' },
208
+ authorName: 'Octo Cat',
209
+ committedAt: '2026-10-01T08:00:00Z',
210
+ url: 'https://github.com/acme/app/commit/abc1234',
211
+ }],
212
+ commitTotal: 1,
172
213
  });
173
214
  });
174
215
 
@@ -1187,6 +1187,24 @@ describe('gh CLI account consent', () => {
1187
1187
  await expect(getOctokitForAccountId('github.com#cli:9')).resolves.toMatchObject({ accountId: 'github.com#cli:9', source: 'cli' });
1188
1188
  });
1189
1189
 
1190
+ it('offers the gh account as a Git transport credential only while the user has switched to it', async () => {
1191
+ const fetch = stubGitHubUsers();
1192
+ const { getGitHubCliCredential } = await import('./octokit.js');
1193
+
1194
+ await expect(getGitHubCliCredential('github.com#cli:9')).resolves.toBeNull();
1195
+ expect(usedCliToken(fetch)).toBe(false);
1196
+
1197
+ auth.setGhCliActive(true);
1198
+ await expect(getGitHubCliCredential('github.com#cli:9')).resolves.toMatchObject({
1199
+ credentialId: 'github.com#cli:9', credentialRevision: 1, providerUserId: 'github.com#9',
1200
+ accessToken: 'cli-token', status: 'valid',
1201
+ });
1202
+ await expect(getGitHubCliCredential('github.com#cli:9', 2)).resolves.toBeNull();
1203
+ // gh signed in as somebody else is not the account the request named.
1204
+ await expect(getGitHubCliCredential('github.com#cli:8')).resolves.toBeNull();
1205
+ await expect(getGitHubCliCredential('github.com#9')).resolves.toBeNull();
1206
+ });
1207
+
1190
1208
  it('stops using the gh token after switching back to a saved account', async () => {
1191
1209
  const fetch = stubGitHubUsers();
1192
1210
  const saved = await auth.setGitHubAuth({ accessToken: 'token-a', user: { id: 7, login: 'saved-user' } });