@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
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { INJECTED_ENV_KEY, assignInjectedEnv, injectedEnvKeys } from './injected-env.js';
4
+
5
+ describe('assignInjectedEnv', () => {
6
+ it('sets the values and records their names', () => {
7
+ const env = { PATH: '/usr/bin' };
8
+
9
+ expect(assignInjectedEnv(env, { OPENCHAMBER_RUNTIME: 'desktop', OPENCHAMBER_DESKTOP_NOTIFY: 'true' })).toBe(env);
10
+
11
+ expect(env).toEqual({
12
+ PATH: '/usr/bin',
13
+ OPENCHAMBER_RUNTIME: 'desktop',
14
+ OPENCHAMBER_DESKTOP_NOTIFY: 'true',
15
+ [INJECTED_ENV_KEY]: 'OPENCHAMBER_RUNTIME,OPENCHAMBER_DESKTOP_NOTIFY',
16
+ });
17
+ expect(injectedEnvKeys(env)).toEqual(new Set(['OPENCHAMBER_RUNTIME', 'OPENCHAMBER_DESKTOP_NOTIFY', INJECTED_ENV_KEY]));
18
+ });
19
+
20
+ it('leaves a value the shell already had as the user\'s', () => {
21
+ const env = { OPENCODE_PASSWORD: 'theirs', OPENCHAMBER_SKIP_API_COMPRESSION: 'false' };
22
+
23
+ assignInjectedEnv(env, {
24
+ OPENCODE_PASSWORD: 'theirs',
25
+ OPENCODE_SERVER_PASSWORD: 'theirs',
26
+ OPENCHAMBER_SKIP_API_COMPRESSION: env.OPENCHAMBER_SKIP_API_COMPRESSION || 'true',
27
+ });
28
+
29
+ expect(env.OPENCHAMBER_SKIP_API_COMPRESSION).toBe('false');
30
+ expect(injectedEnvKeys(env)).toEqual(new Set(['OPENCODE_SERVER_PASSWORD', INJECTED_ENV_KEY]));
31
+ });
32
+
33
+ it('adds to a record inherited from the parent process', () => {
34
+ const env = assignInjectedEnv({}, { OPENCHAMBER_RUNTIME: 'desktop' });
35
+ const child = assignInjectedEnv({ ...env }, { OPENCODE_CONFIG: '/tmp/opencode.json', OPENCHAMBER_RUNTIME: 'desktop' });
36
+
37
+ expect(child[INJECTED_ENV_KEY]).toBe('OPENCHAMBER_RUNTIME,OPENCODE_CONFIG');
38
+ expect(env[INJECTED_ENV_KEY]).toBe('OPENCHAMBER_RUNTIME');
39
+ });
40
+
41
+ it('skips undefined values and writes no record when nothing changed', () => {
42
+ const env = { OPENCHAMBER_HOST: '0.0.0.0' };
43
+
44
+ assignInjectedEnv(env, { OPENCHAMBER_UI_PASSWORD: undefined, OPENCHAMBER_HOST: '0.0.0.0' });
45
+
46
+ expect(env).toEqual({ OPENCHAMBER_HOST: '0.0.0.0' });
47
+ expect(injectedEnvKeys(env).size).toBe(0);
48
+ });
49
+ });
50
+
51
+ describe('injectedEnvKeys', () => {
52
+ it('ignores a malformed record', () => {
53
+ expect(injectedEnvKeys({ [INJECTED_ENV_KEY]: '' }).size).toBe(0);
54
+ expect(injectedEnvKeys({ [INJECTED_ENV_KEY]: ',,' })).toEqual(new Set([INJECTED_ENV_KEY]));
55
+ });
56
+ });
@@ -13,7 +13,7 @@ VS Code omits Linear (`RuntimeAPIs.linear` is optional). Hide Linear UI when the
13
13
  - `packages/web/server/lib/linear/auth.js`: auth file, client id, scopes, redirect URI.
14
14
  - `packages/web/server/lib/linear/oauth.js`: authorization-code + PKCE S256, public callback broker handoff, refresh, revoke.
15
15
  - `packages/web/server/lib/linear/client.js`: GraphQL helper, viewer/organization lookup, and access-token refresh. GraphQL errors prefer `extensions.userPresentableMessage` / validation constraints over the generic `Argument Validation Error` label. User-facing Linear errors set `LinearApiError.userError`. Requests send `public-file-urls-expire-in: 3600` so file URLs in issue descriptions and comments are temporarily readable in the panel.
16
- - `packages/web/server/lib/linear/issues.js`: list/search/get issues, team workflow states, `issueUpdate`, and `commentCreate`. Parses identifiers and Linear URLs. `issueUpdate` resolves identifiers to UUIDs first because Linear's mutation does not accept `ENG-12`. List/get include `state.id`, `priority` (0–4), and labels (`id`, `name`, sanitized hex `color`) so the panel can show them and update status.
16
+ - `packages/web/server/lib/linear/issues.js`: list/search/get issues, team workflow states, `issueUpdate`, and `commentCreate`. Parses identifiers and Linear URLs. `issueUpdate` resolves identifiers to UUIDs first because Linear's mutation does not accept `ENG-12`. List/get include `state.id`, `priority` (0–4), and labels (`id`, `name`, sanitized hex `color`) so the panel can show them and update status. Every issue also carries `parent` (the issue's own fields without labels) and `subIssueProgress` (`{ total, done, more }`, done counts completed, canceled and duplicate states, null without sub-issues). Linear has no sub-issue count and every connection multiplies a query's complexity by its page size (one query may not exceed 10,000 points), so a list asks only the first 20 sub-issue states per issue; `more` says there are others. Get and update add `subIssues` (first 100, related-issue fields, in Linear's `subIssueSortOrder`) and `subIssuesMore`.
17
17
  - `packages/web/server/lib/linear/teams.js`: list Linear teams for mapping UI.
18
18
  - `packages/web/server/lib/linear/mapping.js`: persist default and per-team OpenChamber project paths. Separate from the auth file so disconnect does not wipe maps.
19
19
  - `packages/web/server/lib/linear/status.js`: persist per-session started/completed/failure flags and post the matching Linear comment with an open-session URL. Posts nothing unless the user opted in and the session origin is public; `isPublicSessionOrigin` rejects loopback, private LAN, carrier-grade NAT, link-local and single-label hosts. The dedupe file keeps the newest 500 sessions.
@@ -78,12 +78,11 @@ A worktree started from the panel or picker is created in that mapped project. N
78
78
 
79
79
  - `RuntimeAPIs.linear` is optional. Hide Linear settings, the chat picker, and the panel when it is missing (VS Code).
80
80
  - Store: `packages/ui/src/stores/useLinearAuthStore.ts`. App start refreshes it from `App.tsx` and `MobileApp.tsx`, not `VSCodeApp`.
81
- - Settings: first-party section on the Integrations page. Connect opens the authorization URL and polls status until the workspace list or current `authorizedAt` changes, so Add workspace is not treated as done just because a workspace was already connected. When connected, map a default project and optional per-team projects for the current workspace. Other stored workspaces appear in a list with Switch to. Disconnect removes the current workspace only. The panel can also switch the current workspace when more than one is stored.
82
- - Context panel: desktop/web right-hand rail surface `linear` (`packages/ui/src/components/views/LinearIssuesView.tsx`). Singleton like git/pr. The rail icon is hidden until a Linear workspace is connected; disconnecting while the panel is open closes it. List/search defaults to all issues; the status filter is All, Backlog, To Do, In Progress, In Review, Done, Canceled, and Duplicate, matching the card status order. Identifier/URL still finds completed. Status, assignee, team, and priority filters persist in `useUIStore` so they survive rail switches. Non-default list filters and search tint the filter icon `text-primary`, same as the context rail; one control clears them, not the workspace switch. Changing those filters keeps the previous list until the next page arrives and does not disable the filter row. On a narrow panel search and the filters other than status drop to icons; status keeps its label. The card shows priority and labels. Comments render as an avatar timeline matching the pull request panel, so both context surfaces read alike; comment authors carry `avatarUrl`. The card is read-only except status (`issueUpdate`) and Close (first completed workflow state). Start session stays in a footer while the description and comments scroll. Start session / worktree share `startLinearIssueSession` with the picker. No create-issue, no writing comments, no polling. VS Code and the mobile workspace drawer omit this rail.
83
- - Chat: composer attach menu "Link Linear Issue" attaches body and comments as `linear-issue` context on the next send. Exclusive with a linked GitHub issue or PR. The attached issue is stored on session metadata (`kind: 'linear'`) so work status can show it. Clicking that work-status row opens the Linear rail when Linear is connected on desktop/web; otherwise the Linear URL. Managed Chats do not offer start-from-issue; those sessions have no project directory.
81
+ - Settings: first-party section on the Integrations page. Connect opens the authorization URL and polls status until the workspace list or current `authorizedAt` changes, so Add workspace is not treated as done just because a workspace was already connected. When connected, map a default project and optional per-team projects for the current workspace. Other stored workspaces appear in a list with Switch to. Disconnect removes the current workspace only. The board can also switch the current workspace when more than one is stored.
82
+ - Board: Linear issues list on the issues and PRs board (`packages/ui/src/components/sourceBoard`, desktop/web), next to the repository's issues and PRs. It filters by team, state, whose issues (`assignee=me` or `created`) and priority together, changes an issue's state from its state pill (`issueUpdate`), switches the workspace from the team menu, and starts a session or worktree with the issue attached to the message box. The team's mapped project is where the session starts unless the user picks another. VS Code and the phone shell have their own surfaces (the phone shell opens the board from the sessions menu).
83
+ - Chat: composer attach menu "Link Linear Issue" attaches body and comments as `linear-issue` context on the next send. Exclusive with a linked GitHub issue or PR. The attached issue is stored on session metadata (`kind: 'linear'`) so work status can show it. Clicking that work-status row opens the board searched for the issue when Linear is connected on desktop/web; otherwise the Linear URL. Managed Chats do not offer start-from-issue; those sessions have no project directory.
84
84
  - Worktree: New Worktree can start from a Linear issue. It uses the currently active project and does not consult team-to-project mapping. It shares the reference picker with GitHub (single choice); one issue, PR or Linear issue names the branch.
85
- - Status comments: off until the user turns them on in Settings -> Integrations -> Linear (`LinearSessionComments.tsx`). When on, create-session and worktree-from-Linear post `started` after the session exists. The event hub posts `completed` on the first idle after that, and `failure` on `session.error` except `MessageAbortedError`. Failed comments must not fail session create. Comment bodies are English (they live on Linear) and are one markdown link named `OpenChamber session started` (or completed/failed). Web uses `/?session=<id>` on the current origin; desktop reports the loopback origin its own server listens on, not `openchamber-ui://`. A Linear comment is read by the whole team, so the server posts nothing when that origin is not publicly reachable rather than publishing a link only its author could open. Opening `/?session=` selects that session after the global session list can resolve its directory.
86
- - Magic prompts: `linear.issue.review.visible` / `.instructions`, used only by Start session from the panel and the chat. An issue attached in the composer or from New Worktree carries no instructions.
85
+ - Status comments: off until the user turns them on in Settings -> Integrations -> Linear (`LinearSessionComments.tsx`). When on, a session whose first message carries the issue posts `started` once it is sent. The event hub posts `completed` on the first idle after that, and `failure` on `session.error` except `MessageAbortedError`. Failed comments must not fail session create. Comment bodies are English (they live on Linear) and are one markdown link named `OpenChamber session started` (or completed/failed). Web uses `/?session=<id>` on the current origin; desktop reports the loopback origin its own server listens on, not `openchamber-ui://`. A Linear comment is read by the whole team, so the server posts nothing when that origin is not publicly reachable rather than publishing a link only its author could open. Opening `/?session=` selects that session after the global session list can resolve its directory.
87
86
 
88
87
  ## Notes for contributors
89
88
 
@@ -25,7 +25,7 @@ function readListStatus(value) {
25
25
 
26
26
  function readListAssignee(value) {
27
27
  const assignee = readTrimmedString(value);
28
- if (assignee === 'me' || assignee === 'any') {
28
+ if (assignee === 'me' || assignee === 'created' || assignee === 'any') {
29
29
  return assignee;
30
30
  }
31
31
  return 'any';
@@ -59,6 +59,9 @@ function buildIssueListFilter({ status, assignee, teamId, priority } = {}) {
59
59
  if (resolvedAssignee === 'me') {
60
60
  filter.assignee = { isMe: { eq: true } };
61
61
  }
62
+ if (resolvedAssignee === 'created') {
63
+ filter.creator = { isMe: { eq: true } };
64
+ }
62
65
  if (team) {
63
66
  filter.team = { id: { eq: team } };
64
67
  }
@@ -68,7 +71,9 @@ function buildIssueListFilter({ status, assignee, teamId, priority } = {}) {
68
71
  return Object.keys(filter).length > 0 ? filter : undefined;
69
72
  }
70
73
 
71
- const ISSUE_SUMMARY_FIELDS = `
74
+ // A related issue (a parent, a sub-issue) carries what its row and its own
75
+ // preview header need; labels stay out, they multiply the query's cost.
76
+ const RELATED_ISSUE_FIELDS = `
72
77
  id
73
78
  identifier
74
79
  title
@@ -78,7 +83,26 @@ const ISSUE_SUMMARY_FIELDS = `
78
83
  state { id name type }
79
84
  assignee { name displayName avatarUrl }
80
85
  team { id key name }
86
+ `;
87
+ // Linear has no sub-issue count, and every connection multiplies the query's
88
+ // complexity by its page size (a query may not exceed 10,000 points). A list
89
+ // page asks this many sub-issue states per issue; more reads as "N+".
90
+ const SUB_ISSUE_COUNT_LIMIT = 20;
91
+ const SUB_ISSUE_PAGE = 100;
92
+ const ISSUE_SUMMARY_FIELDS = `
93
+ ${RELATED_ISSUE_FIELDS}
81
94
  labels { nodes { id name color } }
95
+ parent { ${RELATED_ISSUE_FIELDS} }
96
+ children(first: ${SUB_ISSUE_COUNT_LIMIT}) {
97
+ nodes { state { type } }
98
+ pageInfo { hasNextPage }
99
+ }
100
+ `;
101
+ const SUB_ISSUES_FIELDS = `
102
+ subIssues: children(first: ${SUB_ISSUE_PAGE}) {
103
+ nodes { ${RELATED_ISSUE_FIELDS} subIssueSortOrder }
104
+ pageInfo { hasNextPage }
105
+ }
82
106
  `;
83
107
  const LIST_QUERY = `
84
108
  query ListLinearIssues($first: Int!, $after: String, $filter: IssueFilter) {
@@ -100,6 +124,7 @@ const GET_QUERY = `
100
124
  query GetLinearIssue($id: String!) {
101
125
  issue(id: $id) {
102
126
  ${ISSUE_SUMMARY_FIELDS}
127
+ ${SUB_ISSUES_FIELDS}
103
128
  description
104
129
  comments(first: 50) {
105
130
  nodes {
@@ -135,6 +160,7 @@ const ISSUE_UPDATE = `
135
160
  success
136
161
  issue {
137
162
  ${ISSUE_SUMMARY_FIELDS}
163
+ ${SUB_ISSUES_FIELDS}
138
164
  description
139
165
  comments(first: 50) {
140
166
  nodes {
@@ -266,7 +292,7 @@ function readLabels(value) {
266
292
  return nodes.map(readLabel).filter(Boolean);
267
293
  }
268
294
 
269
- function readIssueSummary(node) {
295
+ function readRelatedIssue(node) {
270
296
  if (!isPlainObject(node)) return null;
271
297
  const id = readTrimmedString(node.id);
272
298
  const identifier = readTrimmedString(node.identifier);
@@ -282,11 +308,41 @@ function readIssueSummary(node) {
282
308
  assignee: readAssignee(node.assignee),
283
309
  team: readTeam(node.team),
284
310
  priority: readPriority(node.priority),
285
- labels: readLabels(node.labels),
286
311
  updatedAt: readTrimmedString(node.updatedAt) || null,
287
312
  };
288
313
  }
289
314
 
315
+ const FINISHED_STATE_TYPES = new Set(['completed', 'canceled', 'duplicate']);
316
+
317
+ /** How many sub-issues an issue has and how many are finished; null without any. */
318
+ function readSubIssueProgress(connection) {
319
+ const nodes = isPlainObject(connection) && Array.isArray(connection.nodes) ? connection.nodes : [];
320
+ if (nodes.length === 0) return null;
321
+ const done = nodes.filter((node) => FINISHED_STATE_TYPES.has(readState(node?.state)?.type)).length;
322
+ return { total: nodes.length, done, more: readPageInfo(connection).hasMore };
323
+ }
324
+
325
+ function readIssueSummary(node) {
326
+ const summary = readRelatedIssue(node);
327
+ if (!summary) return null;
328
+ return {
329
+ ...summary,
330
+ labels: readLabels(node.labels),
331
+ parent: readRelatedIssue(node.parent),
332
+ subIssueProgress: readSubIssueProgress(node.children),
333
+ };
334
+ }
335
+
336
+ // Linear's own order: the one the sub-issues are dragged into in Linear.
337
+ function readSubIssues(connection) {
338
+ const nodes = isPlainObject(connection) && Array.isArray(connection.nodes) ? connection.nodes : [];
339
+ return nodes
340
+ .map((node) => ({ issue: readRelatedIssue(node), order: readFiniteNumber(node?.subIssueSortOrder) ?? 0 }))
341
+ .filter((entry) => entry.issue)
342
+ .sort((left, right) => left.order - right.order)
343
+ .map((entry) => entry.issue);
344
+ }
345
+
290
346
  function readComment(node) {
291
347
  if (!isPlainObject(node)) return null;
292
348
  const id = readTrimmedString(node.id);
@@ -318,6 +374,8 @@ function readIssue(node) {
318
374
  ...summary,
319
375
  description: isString(node.description) ? node.description : null,
320
376
  comments,
377
+ subIssues: readSubIssues(node.subIssues),
378
+ subIssuesMore: readPageInfo(node.subIssues).hasMore,
321
379
  };
322
380
  }
323
381
 
@@ -157,6 +157,8 @@ describe('Linear issue list/get', () => {
157
157
  priority: 1,
158
158
  labels: [{ id: 'label-bug', name: 'Bug', color: '#eb5757' }],
159
159
  updatedAt: '2026-09-30T10:00:00.000Z',
160
+ parent: null,
161
+ subIssueProgress: null,
160
162
  }],
161
163
  cursor: 'cursor-2',
162
164
  hasMore: true,
@@ -263,6 +265,18 @@ describe('Linear issue list/get', () => {
263
265
  expect(result.issues).toHaveLength(1);
264
266
  });
265
267
 
268
+ it('lists the issues I created', async () => {
269
+ vi.stubGlobal('fetch', vi.fn(async (_url, options) => {
270
+ expect(JSON.parse(options.body).variables.filter).toEqual({
271
+ state: { type: { nin: ['completed', 'canceled', 'duplicate'] } },
272
+ creator: { isMe: { eq: true } },
273
+ });
274
+ return jsonResponse({ data: { issues: { nodes: [issueNode], pageInfo: { hasNextPage: false, endCursor: null } } } });
275
+ }));
276
+ const result = await listLinearIssues({ assignee: 'created' });
277
+ expect(result.issues).toHaveLength(1);
278
+ });
279
+
266
280
  it('filters each panel status to a Linear state type or name', async () => {
267
281
  const filters = [];
268
282
  vi.stubGlobal('fetch', vi.fn(async (_url, options) => {
@@ -374,6 +388,60 @@ describe('Linear issue list/get', () => {
374
388
  }]);
375
389
  });
376
390
 
391
+ it('reads the parent and how many sub-issues a listed issue has finished', async () => {
392
+ const child = (type) => ({ state: { type } });
393
+ vi.stubGlobal('fetch', vi.fn(async (_url, options) => {
394
+ const body = JSON.parse(options.body);
395
+ expect(body.query).toContain('children(first: 20)');
396
+ return jsonResponse({
397
+ data: {
398
+ issues: {
399
+ nodes: [
400
+ {
401
+ ...issueNode,
402
+ parent: { id: 'issue-uuid-0', identifier: 'ENG-1', title: 'Login revamp', url: 'https://linear.app/openchamber/issue/ENG-1', state: { type: 'started' } },
403
+ children: { nodes: [child('completed'), child('canceled'), child('started')], pageInfo: { hasNextPage: true } },
404
+ },
405
+ { ...issueNode, id: 'issue-uuid-2', identifier: 'ENG-13', parent: null, children: { nodes: [], pageInfo: { hasNextPage: false } } },
406
+ ],
407
+ pageInfo: { hasNextPage: false, endCursor: null },
408
+ },
409
+ },
410
+ });
411
+ }));
412
+
413
+ const result = await listLinearIssues();
414
+ expect(result.issues?.[0]?.parent).toMatchObject({ id: 'issue-uuid-0', identifier: 'ENG-1', title: 'Login revamp' });
415
+ expect(result.issues?.[0]?.parent?.labels).toBeUndefined();
416
+ expect(result.issues?.[0]?.subIssueProgress).toEqual({ total: 3, done: 2, more: true });
417
+ expect(result.issues?.[1]?.parent).toBeNull();
418
+ expect(result.issues?.[1]?.subIssueProgress).toBeNull();
419
+ });
420
+
421
+ it('loads one issue with its sub-issues in Linear order', async () => {
422
+ const sub = (identifier, order) => ({
423
+ id: `uuid-${identifier}`,
424
+ identifier,
425
+ title: `Part ${identifier}`,
426
+ url: `https://linear.app/openchamber/issue/${identifier}`,
427
+ state: { id: 's', name: 'Todo', type: 'unstarted' },
428
+ subIssueSortOrder: order,
429
+ });
430
+ vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
431
+ data: {
432
+ issue: {
433
+ ...issueNode,
434
+ subIssues: { nodes: [sub('ENG-21', 2), sub('ENG-20', -1), { id: '', identifier: 'X' }], pageInfo: { hasNextPage: false } },
435
+ comments: { nodes: [] },
436
+ },
437
+ },
438
+ })));
439
+
440
+ const result = await getLinearIssue('ENG-12');
441
+ expect(result.issue?.subIssues?.map((issue) => issue.identifier)).toEqual(['ENG-20', 'ENG-21']);
442
+ expect(result.issue?.subIssuesMore).toBe(false);
443
+ });
444
+
377
445
  it('creates a comment on the resolved issue UUID', async () => {
378
446
  vi.stubGlobal('fetch', vi.fn(async (_url, options) => {
379
447
  const body = JSON.parse(options.body);
@@ -8,10 +8,10 @@ import {
8
8
  } from './auth.js';
9
9
  import { isPlainObject, isString, readFiniteNumber, readTrimmedString } from './parse.js';
10
10
 
11
- export const LINEAR_AUTHORIZE_URL = 'https://linear.app/oauth/authorize';
12
- export const LINEAR_TOKEN_URL = 'https://api.linear.app/oauth/token';
13
- export const LINEAR_REVOKE_URL = 'https://api.linear.app/oauth/revoke';
14
- export const PENDING_AUTHORIZATION_TTL_MS = 10 * 60_000;
11
+ const LINEAR_AUTHORIZE_URL = 'https://linear.app/oauth/authorize';
12
+ const LINEAR_TOKEN_URL = 'https://api.linear.app/oauth/token';
13
+ const LINEAR_REVOKE_URL = 'https://api.linear.app/oauth/revoke';
14
+ const PENDING_AUTHORIZATION_TTL_MS = 10 * 60_000;
15
15
 
16
16
  const pendingByState = new Map();
17
17
  const brokerPollsByState = new Map();
@@ -24,7 +24,7 @@ export class LinearOAuthError extends Error {
24
24
  }
25
25
  }
26
26
 
27
- export function createPkcePair() {
27
+ function createPkcePair() {
28
28
  const verifier = crypto.randomBytes(32).toString('base64url');
29
29
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
30
30
  return { verifier, challenge };
@@ -1,13 +1,6 @@
1
- export function isString(value) {
2
- return Object.prototype.toString.call(value) === '[object String]';
3
- }
1
+ import { isString } from '../shared/guards.js';
4
2
 
5
- export function isPlainObject(value) {
6
- if (value == null || Array.isArray(value)) {
7
- return false;
8
- }
9
- return Object.getPrototypeOf(value) === Object.prototype;
10
- }
3
+ export { isString, isPlainObject } from '../shared/guards.js';
11
4
 
12
5
  export function readTrimmedString(value) {
13
6
  return isString(value) && value.trim() ? value.trim() : '';
@@ -71,7 +71,29 @@ never replaces editable text or delivered parts. Older items without a summary
71
71
  derive one from the attached comment metadata or the first non-instruction
72
72
  context text. This optional field needs no queue-file migration.
73
73
 
74
- ## Persistence
74
+ ## Scheduled items
75
+
76
+ Scheduled tasks use the same items with an optional `scheduledTask` field
77
+ containing `projectId` and `taskId`. Reuse does not change the target's goal or
78
+ auto-accept policy.
79
+ Only the server can enqueue this provenance. Enqueue deduplicates the project
80
+ and task across all session queues. A scheduled item can omit its model to
81
+ keep the target session selection. Normal composer items still require their
82
+ captured model. The UI retains both kinds in the same projection.
83
+
84
+ Scheduled send guards and terminal result callbacks apply only to scheduled
85
+ items. A definitive before-send target or task refusal with 404 or 409 removes
86
+ that item and lets the next item proceed. This includes removed or retargeted
87
+ tasks and moved targets. Other validation errors and all send failures use the
88
+ existing queue retry policy, including model-switch 404 or 409 responses.
89
+ Successful send reports `sent` without waiting for an assistant reply. Removing,
90
+ clearing or evicting a waiting scheduled item reports `cancelled`. Scheduled
91
+ items can be reordered and removed, but cannot be taken for editing or manual
92
+ send. Bulk take returns only ordinary items and leaves scheduled items queued.
93
+ Disable, delete and retarget use `cancelScheduledTask`. Existing composer
94
+ persistence, retries, directory query and delivery gates remain unchanged.
95
+
96
+ ## Queue file
75
97
 
76
98
  `<data-dir>/message-queue.json` (`OPENCHAMBER_DATA_DIR` or
77
99
  `~/.config/openchamber`): `{ version, revision, sessions: { [sessionId]:
@@ -133,7 +133,11 @@ export const parseQueuedItemInput = (value) => {
133
133
  throw new TypeError('item needs text, attachments, or context');
134
134
  }
135
135
  const sendConfig = parseSendConfig(raw.sendConfig);
136
- if (!sendConfig) throw new TypeError('item sendConfig with providerID and modelID is required');
136
+ const provenance = asRecord(raw.scheduledTask);
137
+ const projectId = asNonEmptyString(provenance?.projectId);
138
+ const taskId = asNonEmptyString(provenance?.taskId);
139
+ if (provenance && (!projectId || !taskId)) throw new TypeError('invalid scheduledTask provenance');
140
+ if (!sendConfig && !provenance) throw new TypeError('item sendConfig with providerID and modelID is required');
137
141
  const item = { content, text };
138
142
  const agentMention = asNonEmptyString(raw.agentMention);
139
143
  if (agentMention) item.agentMention = agentMention;
@@ -141,7 +145,12 @@ export const parseQueuedItemInput = (value) => {
141
145
  item.context = context;
142
146
  const contextPreview = asNonEmptyString(raw.contextPreview).slice(0, 103);
143
147
  if (contextPreview) item.contextPreview = contextPreview;
144
- item.sendConfig = sendConfig;
148
+ item.sendConfig = sendConfig ?? {};
149
+ if (provenance) {
150
+ item.scheduledTask = { projectId, taskId };
151
+ const agent = asNonEmptyString(raw.sendConfig?.agent);
152
+ if (agent) item.sendConfig.agent = agent;
153
+ }
145
154
  return item;
146
155
  };
147
156
 
@@ -177,6 +186,7 @@ const toPublicItem = (item) => {
177
186
  + (contextPreview.length > firstLine.length || firstLine.length > 100 ? '...' : '');
178
187
  }
179
188
  publicItem.sendConfig = { ...item.sendConfig };
189
+ if (item.scheduledTask) publicItem.scheduledTask = { ...item.scheduledTask };
180
190
  return publicItem;
181
191
  };
182
192
 
@@ -228,6 +238,9 @@ export function createMessageQueueRuntime({
228
238
  sessionKnowledgeRuntime = null,
229
239
  broadcastGlobalUiEvent,
230
240
  onPromptSent,
241
+ beforeScheduledTaskSend,
242
+ validateScheduledTaskTarget,
243
+ onScheduledTaskResult,
231
244
  // Resolves the `openchamber/auto` sentinel into a real model and agent right
232
245
  // before the send; absent means the queue never sees the sentinel.
233
246
  resolveAutoSelection = null,
@@ -373,7 +386,16 @@ export function createMessageQueueRuntime({
373
386
  return { revision, session: sessionSnapshot(sessionId) };
374
387
  };
375
388
 
376
- const setQueueItems = (sessionId, directory, items) => {
389
+ const reportScheduledResult = (item, status, error) => {
390
+ if (!item.scheduledTask) return;
391
+ Promise.resolve().then(() => onScheduledTaskResult?.(item.scheduledTask, status, error))
392
+ .catch((failure) => console.warn('[message-queue] scheduled task state write failed:', failure?.message ?? failure));
393
+ };
394
+
395
+ const setQueueItems = (sessionId, directory, items, status = 'cancelled', error) => {
396
+ for (const item of queues.get(sessionId)?.items ?? []) {
397
+ if (item.scheduledTask && !items.some((entry) => entry.id === item.id)) reportScheduledResult(item, status, error);
398
+ }
377
399
  directories.set(sessionId, directory);
378
400
  if (items.length === 0) {
379
401
  queues.delete(sessionId);
@@ -494,13 +516,13 @@ export function createMessageQueueRuntime({
494
516
 
495
517
  // Jev routing, when the queued send named the Auto sentinel. A failure
496
518
  // inside resolves to the fallback model; only a missing fallback throws.
497
- const routed = await resolveAutoSelection?.({
519
+ const routed = providerID && modelID ? await resolveAutoSelection?.({
498
520
  sessionId,
499
521
  directory,
500
522
  model,
501
523
  agent,
502
524
  requestText: item.text,
503
- });
525
+ }) : null;
504
526
  if (routed) {
505
527
  model = routed.model;
506
528
  agent = routed.agent ?? agent;
@@ -508,7 +530,7 @@ export function createMessageQueueRuntime({
508
530
 
509
531
  // v2 selects model and agent on the session, not per prompt: the choice is
510
532
  // switched once and then persists.
511
- await openCodeFetch(`/api/session/${encodeURIComponent(sessionId)}/model`, {
533
+ if (!item.scheduledTask || (providerID && modelID)) await openCodeFetch(`/api/session/${encodeURIComponent(sessionId)}/model`, {
512
534
  directory,
513
535
  method: 'POST',
514
536
  body: { model },
@@ -627,6 +649,19 @@ export function createMessageQueueRuntime({
627
649
 
628
650
  const idle = await isSessionIdle(sessionId, queue.directory);
629
651
  if (idle === null) {
652
+ if (head.scheduledTask && validateScheduledTaskTarget) {
653
+ try {
654
+ await validateScheduledTaskTarget(head.scheduledTask.projectId, sessionId);
655
+ } catch (error) {
656
+ if (error?.statusCode === 404 || error?.statusCode === 409) {
657
+ const after = queues.get(sessionId);
658
+ if (after) setQueueItems(sessionId, after.directory, after.items.filter((entry) => entry.id !== head.id), 'failed', error.message);
659
+ commit(sessionId);
660
+ armDispatch(sessionId);
661
+ return;
662
+ }
663
+ }
664
+ }
630
665
  armDispatch(sessionId, retryDelayMs(1));
631
666
  return;
632
667
  }
@@ -651,9 +686,23 @@ export function createMessageQueueRuntime({
651
686
  sending.set(sessionId, item.id);
652
687
  broadcast(sessionId);
653
688
  try {
689
+ if (item.scheduledTask) {
690
+ try {
691
+ await beforeScheduledTaskSend?.(sessionId, current.directory, item);
692
+ } catch (error) {
693
+ if (error?.statusCode !== 404 && error?.statusCode !== 409) throw error;
694
+ sending.delete(sessionId);
695
+ const after = queues.get(sessionId);
696
+ if (after) setQueueItems(sessionId, after.directory, after.items.filter((entry) => entry.id !== item.id), 'failed', error.message);
697
+ failures.delete(sessionId);
698
+ commit(sessionId);
699
+ armDispatch(sessionId);
700
+ return;
701
+ }
702
+ }
654
703
  await sendItem(sessionId, current.directory, item);
655
704
  const after = queues.get(sessionId);
656
- if (after) setQueueItems(sessionId, after.directory, after.items.filter((entry) => entry.id !== item.id));
705
+ if (after) setQueueItems(sessionId, after.directory, after.items.filter((entry) => entry.id !== item.id), 'sent');
657
706
  failures.delete(sessionId);
658
707
  sending.delete(sessionId);
659
708
  commit(sessionId);
@@ -693,6 +742,16 @@ export function createMessageQueueRuntime({
693
742
  if (!directory) throw new TypeError('directory is required');
694
743
  const parsed = parseQueuedItemInput(itemInput);
695
744
  await load();
745
+ if (parsed.scheduledTask) {
746
+ for (const queue of queues.values()) {
747
+ if (queue.items.some((entry) => entry.scheduledTask?.projectId === parsed.scheduledTask.projectId && entry.scheduledTask.taskId === parsed.scheduledTask.taskId)) {
748
+ throw httpError('task is already queued', 409);
749
+ }
750
+ }
751
+ if ((queues.get(sessionId)?.items.length ?? 0) >= MAX_ITEMS_PER_SESSION || (!queues.has(sessionId) && queues.size >= MAX_SESSIONS)) {
752
+ throw httpError('message queue is full', 409);
753
+ }
754
+ }
696
755
  const item = {
697
756
  id: `queued-${now()}-${Math.random().toString(36).slice(2, 9)}`,
698
757
  createdAt: now(),
@@ -700,7 +759,7 @@ export function createMessageQueueRuntime({
700
759
  };
701
760
  const existing = queues.get(sessionId);
702
761
  const items = [...(existing?.items ?? []), item].slice(-MAX_ITEMS_PER_SESSION);
703
- queues.set(sessionId, { directory, items });
762
+ setQueueItems(sessionId, directory, items);
704
763
  directories.set(sessionId, directory);
705
764
  if (queues.size > MAX_SESSIONS) {
706
765
  const oldest = Array.from(queues.entries())
@@ -708,7 +767,7 @@ export function createMessageQueueRuntime({
708
767
  .sort((left, right) => (left[1].items[0]?.createdAt ?? 0) - (right[1].items[0]?.createdAt ?? 0))
709
768
  .slice(0, queues.size - MAX_SESSIONS);
710
769
  for (const [staleId] of oldest) {
711
- queues.delete(staleId);
770
+ setQueueItems(staleId, directories.get(staleId), []);
712
771
  clearTimer(staleId);
713
772
  broadcast(staleId);
714
773
  directories.delete(staleId);
@@ -741,6 +800,7 @@ export function createMessageQueueRuntime({
741
800
  const queue = queues.get(sessionId);
742
801
  const item = queue?.items.find((entry) => entry.id === itemId);
743
802
  if (!queue || !item) throw httpError('queued message not found', 404);
803
+ if (item.scheduledTask) throw httpError('scheduled messages cannot be taken', 409);
744
804
  setQueueItems(sessionId, queue.directory, queue.items.filter((entry) => entry.id !== itemId));
745
805
  return { ...commit(sessionId), item };
746
806
  };
@@ -752,9 +812,9 @@ export function createMessageQueueRuntime({
752
812
  const queue = queues.get(sessionId);
753
813
  if (!queue) return { revision, session: sessionSnapshot(sessionId), items: [] };
754
814
  const sendingId = sending.get(sessionId) ?? null;
755
- const items = queue.items.filter((item) => item.id !== sendingId);
815
+ const items = queue.items.filter((item) => item.id !== sendingId && !item.scheduledTask);
756
816
  if (items.length === 0) return { revision, session: sessionSnapshot(sessionId), items: [] };
757
- setQueueItems(sessionId, queue.directory, queue.items.filter((item) => item.id === sendingId));
817
+ setQueueItems(sessionId, queue.directory, queue.items.filter((item) => item.id === sendingId || item.scheduledTask));
758
818
  return { ...commit(sessionId), items };
759
819
  };
760
820
 
@@ -810,7 +870,7 @@ export function createMessageQueueRuntime({
810
870
  const deletedSessionId = extractDeletedSessionId(payload);
811
871
  if (deletedSessionId) {
812
872
  if (!queues.has(deletedSessionId)) return;
813
- queues.delete(deletedSessionId);
873
+ setQueueItems(deletedSessionId, directories.get(deletedSessionId), []);
814
874
  clearTimer(deletedSessionId);
815
875
  failures.delete(deletedSessionId);
816
876
  commit(deletedSessionId);
@@ -870,11 +930,23 @@ export function createMessageQueueRuntime({
870
930
  timers.clear();
871
931
  };
872
932
 
933
+ const cancelScheduledTask = async (projectId, taskId) => {
934
+ await load();
935
+ for (const [sessionId, queue] of queues) {
936
+ const items = queue.items.filter((item) => item.scheduledTask?.projectId !== projectId || item.scheduledTask.taskId !== taskId || sending.get(sessionId) === item.id);
937
+ if (items.length === queue.items.length) continue;
938
+ setQueueItems(sessionId, queue.directory, items);
939
+ commit(sessionId);
940
+ armDispatch(sessionId);
941
+ }
942
+ };
943
+
873
944
  return {
874
945
  load,
875
946
  snapshot,
876
947
  sessionSnapshot,
877
948
  enqueue,
949
+ cancelScheduledTask,
878
950
  remove,
879
951
  take,
880
952
  takeAll,
@@ -906,6 +978,7 @@ export function registerMessageQueueRoutes(app, runtime) {
906
978
 
907
979
  app.post('/api/message-queue/sessions/:sessionId/items', async (req, res) => {
908
980
  try {
981
+ if (req.body?.item?.scheduledTask) throw new TypeError('scheduledTask is server-owned');
909
982
  res.json(await runtime.enqueue(req.params.sessionId, req.body?.directory, req.body?.item));
910
983
  } catch (error) {
911
984
  respondError(res, error, 'Failed to queue message');