@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.10

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 (296) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-CzTtVMnI.js} +1 -1
  7. package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-hOi72Wdx.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
  10. package/dist/assets/Icon-B8fSTLdP.js +1 -0
  11. package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-iX_hb_Xt.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-CAMUupwR.js} +2 -2
  16. package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-O4cGdz5p.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
  20. package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-B8d8qIQ7.js} +1 -1
  22. package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
  24. package/dist/assets/appThemeRegistry-BGGHio8T.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-DIIrsMjW.js} +1 -1
  27. package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CnvUeYst.js} +1 -1
  28. package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-BGBUpZEQ.js} +1 -1
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-rvn7_qBM.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-CQ63sc9E.js} +1 -1
  33. package/dist/assets/es-Dsmc0A_3.js +15 -0
  34. package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
  36. package/dist/assets/fr-C_ZlZDI2.js +13 -0
  37. package/dist/assets/gitApi-BF8vuoZc.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-BVu-iykl.js} +1 -1
  40. package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
  41. package/dist/assets/ja-BqqTaWTi.js +15 -0
  42. package/dist/assets/ko-DP2XPM37.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-B7t9lavz.js} +2 -2
  44. package/dist/assets/main-D2BzpnUt.css +1 -0
  45. package/dist/assets/main-DWP-prNB.js +169 -0
  46. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  47. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-D9-B1wdY.js} +1 -1
  48. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  49. package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
  50. package/dist/assets/miniChat-DgOrEQbN.js +2 -0
  51. package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
  52. package/dist/assets/multirun-DelyUO6k.js +4 -0
  53. package/dist/assets/persistence-D0y7226-.js +3 -0
  54. package/dist/assets/pl-BCf5uLlK.js +15 -0
  55. package/dist/assets/platform-CurqO28D.js +6 -0
  56. package/dist/assets/projectMeta-CPElzJ0n.js +3 -0
  57. package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
  58. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  59. package/dist/assets/renderElectronMiniChatApp-pOE93w86.js +2 -0
  60. package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
  61. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  62. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-DSS0KxG3.js} +1 -1
  63. package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
  64. package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
  65. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-Bss8M20i.js} +1 -1
  66. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
  67. package/dist/assets/src-lS1keDVV.css +2 -0
  68. package/dist/assets/store-jaP79mqI.js +15 -0
  69. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-mDR2jXTo.js} +1 -1
  70. package/dist/assets/textarea-XD2oX_Ee.js +1 -0
  71. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-DYeQ_Hvy.js} +1 -1
  72. package/dist/assets/uk-BgFzz8m2.js +15 -0
  73. package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
  74. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
  75. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  76. package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
  77. package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
  78. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-Bjx8zkGM.js} +1 -1
  79. package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
  80. package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
  81. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
  83. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  84. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  85. package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
  86. package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
  87. package/dist/index.html +12 -12
  88. package/dist/mini-chat.html +12 -12
  89. package/dist/mobile.html +12 -12
  90. package/package.json +3 -2
  91. package/server/index.js +359 -63
  92. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  93. package/server/lib/assistants/contracts.js +11 -1
  94. package/server/lib/assistants/projection-history.test.js +265 -18
  95. package/server/lib/assistants/routes.js +6 -0
  96. package/server/lib/assistants/service.js +417 -231
  97. package/server/lib/assistants/service.test.js +392 -148
  98. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  99. package/server/lib/browser-provider/contract.js +312 -0
  100. package/server/lib/browser-provider/host.js +421 -0
  101. package/server/lib/browser-provider/host.test.js +304 -0
  102. package/server/lib/browser-provider/routes.js +122 -0
  103. package/server/lib/browser-provider/routes.test.js +120 -0
  104. package/server/lib/browser-provider/selection-store.js +43 -0
  105. package/server/lib/browser-provider/selection-store.test.js +35 -0
  106. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  107. package/server/lib/conversations/service.js +1 -0
  108. package/server/lib/conversations/service.test.js +16 -0
  109. package/server/lib/conversations/validation.js +11 -1
  110. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  111. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  112. package/server/lib/event-stream/global-hub.js +49 -4
  113. package/server/lib/event-stream/reasoning-projection.js +16 -1
  114. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  115. package/server/lib/event-stream/runtime.js +57 -20
  116. package/server/lib/event-stream/runtime.test.js +112 -0
  117. package/server/lib/llm/DOCUMENTATION.md +33 -9
  118. package/server/lib/llm/catalog.js +1 -1
  119. package/server/lib/llm/completions.js +5 -1
  120. package/server/lib/llm/completions.test.js +3 -3
  121. package/server/lib/llm/generate.js +237 -145
  122. package/server/lib/llm/generate.test.js +201 -46
  123. package/server/lib/llm/routes.js +2 -0
  124. package/server/lib/message-queue/runtime.js +15 -2
  125. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  126. package/server/lib/notifications/runtime.js +133 -31
  127. package/server/lib/notifications/runtime.test.js +103 -0
  128. package/server/lib/notifications/template-runtime.js +130 -38
  129. package/server/lib/notifications/template-runtime.test.js +96 -0
  130. package/server/lib/opencode/DOCUMENTATION.md +89 -17
  131. package/server/lib/opencode/agent-document.d.ts +28 -0
  132. package/server/lib/opencode/agent-document.js +280 -0
  133. package/server/lib/opencode/agent-document.test.js +137 -0
  134. package/server/lib/opencode/agents.js +96 -0
  135. package/server/lib/opencode/auth-state-runtime.js +5 -0
  136. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  137. package/server/lib/opencode/config-entity-routes.js +42 -99
  138. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  139. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  140. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  141. package/server/lib/opencode/ensure-cli.js +1 -2
  142. package/server/lib/opencode/env-runtime.js +40 -26
  143. package/server/lib/opencode/env-runtime.test.js +10 -17
  144. package/server/lib/opencode/feature-routes-runtime.js +53 -4
  145. package/server/lib/opencode/feature-routes-runtime.test.js +25 -0
  146. package/server/lib/opencode/http-server-connections.js +98 -0
  147. package/server/lib/opencode/http-server-connections.test.js +321 -0
  148. package/server/lib/opencode/lifecycle.js +349 -39
  149. package/server/lib/opencode/lifecycle.test.js +322 -0
  150. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  151. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  152. package/server/lib/opencode/openchamber-routes.js +7 -1
  153. package/server/lib/opencode/opencode2-pin.js +4 -4
  154. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  155. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  156. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  157. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  158. package/server/lib/opencode/plugin-routes.js +3 -18
  159. package/server/lib/opencode/plugin-routes.test.js +9 -9
  160. package/server/lib/opencode/provider-catalog.js +58 -12
  161. package/server/lib/opencode/provider-catalog.test.js +48 -0
  162. package/server/lib/opencode/proxy.js +254 -46
  163. package/server/lib/opencode/proxy.test.js +238 -5
  164. package/server/lib/opencode/routes.js +326 -62
  165. package/server/lib/opencode/routes.test.js +278 -12
  166. package/server/lib/opencode/runtime-contract.js +401 -0
  167. package/server/lib/opencode/runtime-contract.test.js +214 -0
  168. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  169. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  170. package/server/lib/opencode/server-utils-runtime.js +33 -0
  171. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  172. package/server/lib/opencode/shared-service.js +147 -0
  173. package/server/lib/opencode/shared-service.test.js +93 -0
  174. package/server/lib/opencode/shared.js +3 -3
  175. package/server/lib/opencode/shutdown-runtime.js +15 -6
  176. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  177. package/server/lib/opencode/skill-routes.js +20 -33
  178. package/server/lib/opencode/skill-routes.test.js +35 -2
  179. package/server/lib/opencode/skills.test.js +15 -0
  180. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  181. package/server/lib/opencode/static-routes-runtime.js +3 -1
  182. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  183. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  184. package/server/lib/opencode/upgrade-screen.js +299 -0
  185. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +73 -18
  211. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  212. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  213. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  214. package/server/lib/session-metadata/routes.js +112 -9
  215. package/server/lib/session-metadata/routes.test.js +47 -2
  216. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  217. package/server/lib/session-metadata/session-archive.js +654 -0
  218. package/server/lib/session-metadata/session-archive.test.js +359 -0
  219. package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
  220. package/server/lib/session-metadata/session-metadata-store.js +270 -83
  221. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  222. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  223. package/server/lib/session-metadata/session-projection.js +279 -0
  224. package/server/lib/session-metadata/session-projection.test.js +64 -0
  225. package/server/lib/session-metadata/system-session.js +55 -0
  226. package/server/lib/session-metadata/system-session.test.js +55 -0
  227. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  228. package/server/lib/session-title/native-runtime.test.js +167 -0
  229. package/server/lib/session-title/runtime.js +69 -44
  230. package/server/lib/session-title/session-access.js +46 -0
  231. package/server/lib/session-title/session-access.test.js +91 -0
  232. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  233. package/server/lib/session-turn-pages/routes.js +17 -102
  234. package/server/lib/session-turn-pages/routes.test.js +281 -0
  235. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  236. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  237. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  238. package/server/lib/small-model/catalog.js +31 -51
  239. package/server/lib/small-model/catalog.test.js +65 -10
  240. package/server/lib/small-model/custom-api.js +68 -0
  241. package/server/lib/small-model/custom-api.test.js +64 -0
  242. package/server/lib/small-model/index.js +124 -240
  243. package/server/lib/small-model/resolve.js +35 -120
  244. package/server/lib/small-model/resolve.test.js +20 -110
  245. package/server/lib/small-model/routes.js +1 -3
  246. package/server/lib/small-model/summary-settings.test.js +157 -253
  247. package/server/opencode-proxy.test.js +2 -0
  248. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  249. package/dist/assets/Icon-DD-BuMfZ.js +0 -1
  250. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +0 -2
  251. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  252. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  253. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  254. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  255. package/dist/assets/TerminalView-Dh9MWCpS.js +0 -8
  256. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  257. package/dist/assets/es-c52LTHHe.js +0 -15
  258. package/dist/assets/fileMentionTouchSelection-BvrNCroG.js +0 -4
  259. package/dist/assets/fr-PisFbUE8.js +0 -13
  260. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  261. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  262. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  263. package/dist/assets/ko-ClFys5o2.js +0 -15
  264. package/dist/assets/main-Dn9rpsgl.js +0 -169
  265. package/dist/assets/main-qSeeJRW1.css +0 -1
  266. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  267. package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
  268. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  269. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  270. package/dist/assets/multirun-BvSG94mW.js +0 -4
  271. package/dist/assets/persistence-Ce3AnpEr.js +0 -3
  272. package/dist/assets/pl-CnaKWN5F.js +0 -15
  273. package/dist/assets/platform-CWTIp02q.js +0 -4
  274. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  275. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  276. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  277. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  278. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  279. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  280. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  281. package/dist/assets/src-BzX5UI-c.css +0 -2
  282. package/dist/assets/store-DJczmczv.js +0 -15
  283. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  284. package/dist/assets/uk-CaX0iqkB.js +0 -15
  285. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  286. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  287. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  288. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  289. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  290. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  291. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  292. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  293. package/server/lib/small-model/call.js +0 -427
  294. package/server/lib/small-model/call.test.js +0 -382
  295. package/server/lib/small-model/opencode-session.js +0 -183
  296. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -1,3 +1,41 @@
1
+ import { readNotificationSessionRecord } from './template-runtime.js';
2
+
3
+ const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
4
+
5
+ const readNotificationEventFields = (payload) => {
6
+ const properties = payload?.properties && typeof payload.properties === 'object' && !Array.isArray(payload.properties)
7
+ ? payload.properties
8
+ : {};
9
+ const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
10
+ ? payload.data
11
+ : {};
12
+ const form = (properties.form && typeof properties.form === 'object' ? properties.form : null)
13
+ || (data.form && typeof data.form === 'object' ? data.form : null);
14
+ const questions = Array.isArray(properties.questions)
15
+ ? properties.questions
16
+ : (Array.isArray(data.questions) ? data.questions : []);
17
+ const firstQuestion = questions[0] && typeof questions[0] === 'object' ? questions[0] : null;
18
+ const field = Array.isArray(form?.fields)
19
+ ? form.fields.find((item) => item && typeof item === 'object' && item.hidden !== true)
20
+ : null;
21
+ return {
22
+ requestId: trimmedText(properties.id)
23
+ || trimmedText(properties.requestID)
24
+ || trimmedText(properties.requestId)
25
+ || trimmedText(data.id)
26
+ || trimmedText(data.requestID)
27
+ || trimmedText(data.requestId)
28
+ || trimmedText(form?.id),
29
+ permission: trimmedText(properties.permission) || trimmedText(data.permission) || trimmedText(data.action),
30
+ message: trimmedText(data.message) || trimmedText(properties.message),
31
+ header: trimmedText(firstQuestion?.header) || trimmedText(field?.title) || trimmedText(form?.title),
32
+ questionText: trimmedText(firstQuestion?.question)
33
+ || trimmedText(field?.description)
34
+ || trimmedText(field?.title)
35
+ || trimmedText(form?.title),
36
+ };
37
+ };
38
+
1
39
  export const createNotificationTriggerRuntime = (deps) => {
2
40
  const {
3
41
  readSettingsFromDisk,
@@ -255,14 +293,14 @@ export const createNotificationTriggerRuntime = (deps) => {
255
293
  if (!response.ok) {
256
294
  return cached;
257
295
  }
258
- const session = await response.json().catch(() => null);
259
- if (!session || typeof session !== 'object') {
296
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
297
+ if (!session) {
260
298
  return cached;
261
299
  }
262
300
 
263
301
  const meta = {
264
302
  parentID: normalizeParentID(session.parentID),
265
- title: typeof session.title === 'string' ? session.title : null,
303
+ title: trimmedText(session.title) || null,
266
304
  complete: true,
267
305
  ...metaFromOpenchamber(session.metadata?.openchamber),
268
306
  };
@@ -346,13 +384,24 @@ export const createNotificationTriggerRuntime = (deps) => {
346
384
 
347
385
  const extractSessionIdFromPayload = (payload) => {
348
386
  if (!payload || typeof payload !== 'object') return null;
349
- const props = payload.properties;
387
+ const props = payload.properties && typeof payload.properties === 'object'
388
+ ? payload.properties
389
+ : (payload.data && typeof payload.data === 'object' ? payload.data : null);
350
390
  const info = props?.info;
391
+ const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
392
+ ? payload.data
393
+ : null;
351
394
  const sessionId =
352
395
  info?.sessionID ??
353
396
  info?.sessionId ??
354
397
  props?.sessionID ??
355
398
  props?.sessionId ??
399
+ props?.form?.sessionID ??
400
+ props?.form?.sessionId ??
401
+ data?.sessionID ??
402
+ data?.sessionId ??
403
+ data?.form?.sessionID ??
404
+ data?.form?.sessionId ??
356
405
  props?.session ??
357
406
  null;
358
407
  return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
@@ -360,11 +409,32 @@ export const createNotificationTriggerRuntime = (deps) => {
360
409
 
361
410
  const extractDirectoryFromPayload = (payload) => {
362
411
  if (!payload || typeof payload !== 'object') return undefined;
363
- const props = payload.properties;
364
- const directory = props?.directory ?? props?.info?.directory;
365
- if (typeof directory !== 'string') return undefined;
366
- const trimmed = directory.trim();
367
- return trimmed.length > 0 ? trimmed : undefined;
412
+ const props = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
413
+ const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
414
+ const info = props.info && typeof props.info === 'object' ? props.info : {};
415
+ const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
416
+ const candidates = [
417
+ props.directory,
418
+ info.directory,
419
+ info.location?.directory,
420
+ payload.location?.directory,
421
+ data.directory,
422
+ data.location?.directory,
423
+ dataInfo.directory,
424
+ dataInfo.location?.directory,
425
+ ];
426
+ for (const value of candidates) {
427
+ const directory = trimmedText(value);
428
+ if (directory) return directory;
429
+ }
430
+ return undefined;
431
+ };
432
+
433
+ const resolvePushSessionName = async (variables, sessionId, directory) => {
434
+ const fromTemplate = trimmedText(variables?.session_name);
435
+ if (fromTemplate) return fromTemplate;
436
+ const meta = await fetchSessionMeta(sessionId, directory);
437
+ return trimmedText(meta?.title);
368
438
  };
369
439
 
370
440
  // A session with an ACTIVE goal suppresses per-turn ready notifications;
@@ -381,7 +451,7 @@ export const createNotificationTriggerRuntime = (deps) => {
381
451
  signal: AbortSignal.timeout(2000),
382
452
  });
383
453
  if (!response.ok) return false;
384
- const session = await response.json().catch(() => null);
454
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
385
455
  const goal = session?.metadata?.openchamber?.goal;
386
456
  return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
387
457
  } catch {
@@ -412,20 +482,36 @@ export const createNotificationTriggerRuntime = (deps) => {
412
482
  }
413
483
  }
414
484
  }
415
- if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
416
- const error = payload.properties?.error;
485
+ // Ticket 07: shutdown interrupt is not a completion terminal — no ready push.
486
+ if (payload.type === 'session.execution.interrupted') {
487
+ const reason = payload.properties?.reason ?? payload.data?.reason;
488
+ if (reason === 'shutdown') return;
489
+ }
490
+ // v2 completion is session.execution.succeeded. session.idle is a deprecated
491
+ // fallback; the ready-notification cooldown below collapses the pair.
492
+ if ((
493
+ payload.type === 'session.idle'
494
+ || payload.type === 'session.execution.succeeded'
495
+ || payload.type === 'session.error'
496
+ || payload.type === 'session.execution.failed'
497
+ ) && sessionId) {
498
+ const body = payload.properties && typeof payload.properties === 'object'
499
+ ? payload.properties
500
+ : (payload.data && typeof payload.data === 'object' ? payload.data : {});
501
+ const error = body?.error;
417
502
  const errorText = typeof error?.message === 'string'
418
503
  ? error.message
419
504
  : typeof error === 'string' ? error : '';
505
+ const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
420
506
  await maybeSendPushForTrigger({
421
507
  ...payload,
422
508
  type: 'message.updated',
423
509
  properties: {
424
- ...payload.properties,
510
+ ...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
425
511
  info: {
426
512
  sessionID: sessionId,
427
513
  role: 'assistant',
428
- finish: payload.type === 'session.error' ? 'error' : 'stop',
514
+ finish: failed ? 'error' : 'stop',
429
515
  ...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
430
516
  },
431
517
  },
@@ -487,12 +573,12 @@ export const createNotificationTriggerRuntime = (deps) => {
487
573
  const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
488
574
 
489
575
  const variables = await buildTemplateVariables(payload, sessionId);
490
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
576
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
491
577
 
492
578
  const messageId = info?.id;
493
579
  let lastMessage = extractLastMessageText(payload);
494
580
  if (!lastMessage) {
495
- lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
581
+ lastMessage = await fetchLastAssistantMessageText(sessionId, messageId, undefined, notificationDirectory);
496
582
  }
497
583
 
498
584
  variables.last_message = await prepareNotificationLastMessage({
@@ -543,7 +629,21 @@ export const createNotificationTriggerRuntime = (deps) => {
543
629
  return;
544
630
  }
545
631
 
546
- if (payload.type === 'question.asked' && sessionId) {
632
+ if ((
633
+ payload.type === 'form.replied'
634
+ || payload.type === 'form.cancelled'
635
+ || payload.type === 'question.replied'
636
+ || payload.type === 'question.rejected'
637
+ ) && sessionId) {
638
+ const existingTimer = pushQuestionDebounceTimers.get(sessionId);
639
+ if (existingTimer) {
640
+ clearTimeout(existingTimer);
641
+ pushQuestionDebounceTimers.delete(sessionId);
642
+ }
643
+ return;
644
+ }
645
+
646
+ if ((payload.type === 'question.asked' || payload.type === 'form.created') && sessionId) {
547
647
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
548
648
  return;
549
649
  }
@@ -564,9 +664,9 @@ export const createNotificationTriggerRuntime = (deps) => {
564
664
  return;
565
665
  }
566
666
 
567
- const firstQuestion = payload.properties?.questions?.[0];
568
- const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
569
- const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
667
+ const prompt = readNotificationEventFields(payload);
668
+ const header = prompt.header;
669
+ const questionText = prompt.questionText;
570
670
 
571
671
  let title = 'Needs your answer';
572
672
  let body = '';
@@ -574,7 +674,7 @@ export const createNotificationTriggerRuntime = (deps) => {
574
674
 
575
675
  try {
576
676
  const variables = await buildTemplateVariables(payload, sessionId);
577
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
677
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
578
678
  variables.last_message = questionText || header || '';
579
679
 
580
680
  const templates = settings.notificationTemplates || {};
@@ -625,8 +725,8 @@ export const createNotificationTriggerRuntime = (deps) => {
625
725
  }
626
726
 
627
727
  if (payload.type === 'permission.replied' && sessionId) {
628
- const requestId = payload.properties?.requestID ?? payload.properties?.requestId ?? payload.properties?.id;
629
- const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
728
+ const requestId = readNotificationEventFields(payload).requestId;
729
+ const requestKey = requestId ? `${sessionId}:${requestId}` : null;
630
730
  const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
631
731
  if (!pendingNotification) {
632
732
  return;
@@ -646,9 +746,10 @@ export const createNotificationTriggerRuntime = (deps) => {
646
746
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
647
747
  return;
648
748
  }
649
- const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
650
- const permission = payload.properties?.permission;
651
- const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
749
+ const permissionFields = readNotificationEventFields(payload);
750
+ const requestId = permissionFields.requestId;
751
+ const permission = permissionFields.permission;
752
+ const requestKey = requestId ? `${sessionId}:${requestId}` : null;
652
753
  if (requestKey && notifiedPermissionRequests.has(requestKey)) {
653
754
  return;
654
755
  }
@@ -686,11 +787,12 @@ export const createNotificationTriggerRuntime = (deps) => {
686
787
  return;
687
788
  }
688
789
 
689
- const sessionTitle = payload.properties?.sessionTitle;
790
+ const sessionTitle = payload.properties?.sessionTitle ?? payload.data?.sessionTitle;
690
791
  const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
792
+ const detail = permissionFields.message;
691
793
  const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
692
794
  ? sessionTitle.trim()
693
- : permissionText || 'Agent is waiting for your approval';
795
+ : detail || permissionText || 'Agent is waiting for your approval';
694
796
 
695
797
  let title = 'Needs permission';
696
798
  let body = '';
@@ -698,7 +800,7 @@ export const createNotificationTriggerRuntime = (deps) => {
698
800
 
699
801
  try {
700
802
  const variables = await buildTemplateVariables(payload, sessionId);
701
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
803
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
702
804
  variables.last_message = fallbackMessage;
703
805
 
704
806
  const templates = settings.notificationTemplates || {};
@@ -766,8 +868,8 @@ export const createNotificationTriggerRuntime = (deps) => {
766
868
  signal: AbortSignal.timeout(2000),
767
869
  });
768
870
  if (response.ok) {
769
- const session = await response.json().catch(() => null);
770
- if (typeof session?.title === 'string') sessionName = session.title.trim();
871
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
872
+ sessionName = trimmedText(session?.title);
771
873
  }
772
874
  } catch {
773
875
  // Session name is presentation sugar for the mobile push — never block on it.
@@ -457,6 +457,17 @@ describe('contact turn notifications', () => {
457
457
  });
458
458
 
459
459
  describe('notification trigger live activity end', () => {
460
+ it('treats session.execution.succeeded as completion, including a v2 data envelope', async () => {
461
+ vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse('ses_done')));
462
+ const { runtime, sendLiveActivityEnd, emitDesktopNotification } = createRuntime();
463
+ await runtime.maybeSendPushForTrigger({
464
+ type: 'session.execution.succeeded',
465
+ data: { sessionID: 'ses_done' },
466
+ });
467
+ expect(sendLiveActivityEnd).toHaveBeenCalledWith({ sessionId: 'ses_done', status: 'complete' });
468
+ expect(emitDesktopNotification).toHaveBeenCalled();
469
+ });
470
+
460
471
  it('ends the live activity on top-level completion and error', async () => {
461
472
  vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
462
473
  const { runtime, sendLiveActivityEnd } = createRuntime();
@@ -615,6 +626,97 @@ describe('notification event toggles', () => {
615
626
  expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
616
627
  });
617
628
 
629
+ it('notifies on an OpenCode 2 question form and a permission request', async () => {
630
+ vi.useFakeTimers();
631
+ vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
632
+ data: { id: 'ses_ask', parentID: null, title: '发布说明', metadata: {} },
633
+ })));
634
+ const resolveNotificationTemplate = vi.fn((template, variables) => (
635
+ typeof template === 'string'
636
+ ? template.replace(/\{(\w+)\}/g, (_match, key) => variables?.[key] ?? '')
637
+ : template
638
+ ));
639
+ const { runtime, emitDesktopNotification, sendApnsToAllUiSessions } = createRuntime({
640
+ buildTemplateVariables: vi.fn(async () => ({ session_name: '' })),
641
+ resolveNotificationTemplate,
642
+ readSettingsFromDisk: vi.fn(async () => ({
643
+ ...defaultSettings,
644
+ notificationTemplates: {
645
+ question: { title: 'Needs your answer', message: '{last_message}' },
646
+ },
647
+ })),
648
+ });
649
+
650
+ await runtime.maybeSendPushForTrigger({
651
+ type: 'form.created',
652
+ data: {
653
+ form: {
654
+ id: 'frm_1',
655
+ sessionID: 'ses_ask',
656
+ title: 'Questions',
657
+ metadata: { kind: 'question' },
658
+ fields: [{ key: 'q0', title: '确认入口', description: '从哪个入口触发?' }],
659
+ },
660
+ },
661
+ });
662
+ await vi.runAllTimersAsync();
663
+
664
+ expect(emitDesktopNotification).toHaveBeenCalledWith(expect.objectContaining({
665
+ kind: 'question',
666
+ body: '从哪个入口触发?',
667
+ }));
668
+ expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(
669
+ expect.objectContaining({ type: 'question', sessionName: '发布说明' }),
670
+ { requireNoSse: true },
671
+ );
672
+
673
+ emitDesktopNotification.mockClear();
674
+ sendApnsToAllUiSessions.mockClear();
675
+ await runtime.maybeSendPushForTrigger({
676
+ type: 'permission.asked',
677
+ data: {
678
+ id: 'per_1',
679
+ sessionID: 'ses_ask',
680
+ action: 'edit',
681
+ message: '修改 README',
682
+ resources: ['README.md'],
683
+ },
684
+ });
685
+ await vi.runAllTimersAsync();
686
+
687
+ expect(emitDesktopNotification).toHaveBeenCalledWith(expect.objectContaining({
688
+ kind: 'permission',
689
+ body: '修改 README',
690
+ }));
691
+ expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(
692
+ expect.objectContaining({ type: 'permission', sessionName: '发布说明' }),
693
+ { requireNoSse: true },
694
+ );
695
+ });
696
+
697
+ it('uses the wrapped OpenCode 2 session title when the template has none', async () => {
698
+ vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
699
+ data: {
700
+ id: 'ses_done',
701
+ parentID: null,
702
+ title: '发布说明',
703
+ metadata: {},
704
+ },
705
+ })));
706
+ const { runtime, sendApnsToAllUiSessions } = createRuntime({
707
+ buildTemplateVariables: vi.fn(async () => ({ session_name: '' })),
708
+ });
709
+ await runtime.maybeSendPushForTrigger({
710
+ type: 'session.execution.succeeded',
711
+ location: { directory: '/repo' },
712
+ data: { sessionID: 'ses_done' },
713
+ });
714
+ expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(
715
+ expect.objectContaining({ type: 'ready', sessionName: '发布说明' }),
716
+ { requireNoSse: true },
717
+ );
718
+ });
719
+
618
720
  it('still sends goal settle push by default', async () => {
619
721
  vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ id: 'ses_goal', title: 'Goal run' })));
620
722
  const { runtime, sendPushToAllUiSessions } = createRuntime();
@@ -626,6 +728,7 @@ describe('notification event toggles', () => {
626
728
  });
627
729
  expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
628
730
  expect(sendPushToAllUiSessions.mock.calls[0][0]).toMatchObject({ tag: 'goal-ses_goal' });
731
+ expect(sendPushToAllUiSessions.mock.calls[0][0].data).toMatchObject({ sessionName: 'Goal run' });
629
732
  });
630
733
  });
631
734
 
@@ -1,5 +1,75 @@
1
+ import { projectSessionMessageRecords } from '../session-turn-pages/session-message-projection.js';
1
2
  import { summarizeText as summarizeSharedText } from '../text/summarization.js';
2
3
 
4
+ const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
5
+
6
+ const isSessionRecord = (value) => {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
8
+ return typeof value.id === 'string'
9
+ || typeof value.sessionID === 'string'
10
+ || typeof value.title === 'string'
11
+ || typeof value.parentID === 'string'
12
+ || (value.metadata && typeof value.metadata === 'object');
13
+ };
14
+
15
+ /**
16
+ * OpenCode 2 `GET /api/session/:id` returns `{ data: Session }`.
17
+ * Legacy responses and tests still return the session object itself.
18
+ * @param {unknown} payload
19
+ * @returns {object | null}
20
+ */
21
+ export const readNotificationSessionRecord = (payload) => {
22
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
23
+ if (isSessionRecord(payload.data)) return payload.data;
24
+ return isSessionRecord(payload) ? payload : null;
25
+ };
26
+
27
+ const TITLE_EVENT_TYPES = new Set(['session.created', 'session.updated', 'session.renamed']);
28
+
29
+ /**
30
+ * Title carried by a session lifecycle event.
31
+ * v2 puts it on `data.title` (`session.created` / `session.renamed`).
32
+ * Legacy events put it on `properties.info.title`.
33
+ * @param {object | null | undefined} payload
34
+ * @returns {{ sessionId: string, title: string } | null}
35
+ */
36
+ export const readSessionTitleFromEvent = (payload) => {
37
+ if (!payload || typeof payload !== 'object' || !TITLE_EVENT_TYPES.has(payload.type)) return null;
38
+ const bodies = [payload.properties, payload.data].filter((body) => body && typeof body === 'object' && !Array.isArray(body));
39
+ for (const body of bodies) {
40
+ const info = body.info && typeof body.info === 'object' ? body.info : null;
41
+ const title = trimmedText(info?.title) || trimmedText(body.title);
42
+ const sessionId = trimmedText(info?.id)
43
+ || trimmedText(info?.sessionID)
44
+ || trimmedText(body.sessionID)
45
+ || trimmedText(body.id);
46
+ if (title && sessionId) return { sessionId, title };
47
+ }
48
+ return null;
49
+ };
50
+
51
+ const readNotificationDirectory = (payload) => {
52
+ const properties = payload?.properties && typeof payload.properties === 'object' ? payload.properties : {};
53
+ const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
54
+ const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
55
+ const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
56
+ const candidates = [
57
+ properties.directory,
58
+ info.directory,
59
+ info.location?.directory,
60
+ payload?.location?.directory,
61
+ data.directory,
62
+ data.location?.directory,
63
+ dataInfo.directory,
64
+ dataInfo.location?.directory,
65
+ ];
66
+ for (const value of candidates) {
67
+ const directory = trimmedText(value);
68
+ if (directory) return directory;
69
+ }
70
+ return '';
71
+ };
72
+
3
73
  export const createNotificationTemplateRuntime = (deps) => {
4
74
  const {
5
75
  readSettingsFromDisk,
@@ -133,37 +203,45 @@ export const createNotificationTemplateRuntime = (deps) => {
133
203
  return '';
134
204
  };
135
205
 
136
- const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS) => {
206
+ const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS, directory) => {
137
207
  if (!sessionId) return '';
138
208
 
139
209
  try {
140
- const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
141
- const response = await fetch(`${url}?limit=5`, {
210
+ const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
211
+ const params = new URLSearchParams({ limit: '5', order: 'desc' });
212
+ const scopedDirectory = trimmedText(directory);
213
+ if (scopedDirectory) params.set('directory', scopedDirectory);
214
+ const response = await fetch(`${base}?${params.toString()}`, {
142
215
  method: 'GET',
143
216
  headers: {
144
217
  Accept: 'application/json',
145
- ...getOpenCodeAuthHeaders(),
218
+ ...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
146
219
  },
147
220
  signal: AbortSignal.timeout(3000),
148
221
  });
149
222
 
150
223
  if (!response.ok) return '';
151
224
 
152
- const messages = await response.json().catch(() => null);
153
- if (!Array.isArray(messages)) return '';
225
+ const payload = await response.json().catch(() => null);
226
+ // Legacy lists are a chronological array. OpenCode 2 returns `{ data }`
227
+ // and `order=desc` puts the newest turn first.
228
+ const newestFirst = !Array.isArray(payload);
229
+ const records = Array.isArray(payload)
230
+ ? payload
231
+ : (Array.isArray(payload?.data) ? payload.data : null);
232
+ if (!records) return '';
233
+ const projected = projectSessionMessageRecords(records);
234
+ const messages = newestFirst ? projected : [...projected].reverse();
154
235
 
155
236
  let target = null;
156
237
  if (messageId) {
157
238
  target = messages.find((message) => message?.info?.id === messageId && message?.info?.role === 'assistant');
158
239
  }
159
240
  if (!target) {
160
- for (let i = messages.length - 1; i >= 0; i -= 1) {
161
- const message = messages[i];
162
- if (message?.info?.role === 'assistant' && message?.info?.finish === 'stop') {
163
- target = message;
164
- break;
165
- }
166
- }
241
+ target = messages.find((message) => (
242
+ message?.info?.role === 'assistant'
243
+ && extractTextFromParts(message.parts, maxLength).length > 0
244
+ ));
167
245
  }
168
246
 
169
247
  if (!target || !Array.isArray(target.parts)) return '';
@@ -175,9 +253,9 @@ export const createNotificationTemplateRuntime = (deps) => {
175
253
  };
176
254
 
177
255
  const cacheSessionTitle = (sessionId, title) => {
178
- if (typeof sessionId === 'string' && sessionId.length > 0 && typeof title === 'string' && title.length > 0) {
179
- sessionTitleCache.set(sessionId, title);
180
- }
256
+ const id = trimmedText(sessionId);
257
+ const name = trimmedText(title);
258
+ if (id && name) sessionTitleCache.set(id, name);
181
259
  };
182
260
 
183
261
  const getCachedSessionTitle = (sessionId) => {
@@ -185,15 +263,12 @@ export const createNotificationTemplateRuntime = (deps) => {
185
263
  };
186
264
 
187
265
  const maybeCacheSessionInfoFromEvent = (payload) => {
188
- if (!payload || typeof payload !== 'object') return;
189
- const type = payload.type;
190
- if (type !== 'session.updated' && type !== 'session.created') return;
191
- const info = payload.properties?.info;
192
- if (!info || typeof info !== 'object') return;
193
- cacheSessionTitle(info.id, info.title);
266
+ const titled = readSessionTitleFromEvent(payload);
267
+ if (!titled) return;
268
+ cacheSessionTitle(titled.sessionId, titled.title);
194
269
  };
195
270
 
196
- const fetchSessionInfo = async (sessionId) => {
271
+ const fetchSessionInfo = async (sessionId, directory) => {
197
272
  if (!sessionId) return null;
198
273
 
199
274
  const cached = sessionInfoCache.get(sessionId);
@@ -202,20 +277,24 @@ export const createNotificationTemplateRuntime = (deps) => {
202
277
  }
203
278
 
204
279
  try {
205
- const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
280
+ const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
281
+ const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
206
282
  const response = await fetch(url, {
207
283
  method: 'GET',
208
- headers: { Accept: 'application/json' },
284
+ headers: {
285
+ Accept: 'application/json',
286
+ ...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
287
+ },
209
288
  signal: AbortSignal.timeout(2000),
210
289
  });
211
290
  if (!response.ok) {
212
291
  console.warn(`[Notification] fetchSessionInfo: ${response.status} for session ${sessionId}`);
213
292
  return null;
214
293
  }
215
- const data = await response.json().catch(() => null);
216
- if (data && typeof data === 'object') {
217
- sessionInfoCache.set(sessionId, { data, at: Date.now() });
218
- return data;
294
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
295
+ if (session) {
296
+ sessionInfoCache.set(sessionId, { data: session, at: Date.now() });
297
+ return session;
219
298
  }
220
299
  return null;
221
300
  } catch (error) {
@@ -227,7 +306,11 @@ export const createNotificationTemplateRuntime = (deps) => {
227
306
  const buildTemplateVariables = async (payload, sessionId) => {
228
307
  const info = payload?.properties?.info || {};
229
308
 
230
- let sessionTitle = payload?.properties?.sessionTitle || payload?.properties?.session?.title || (typeof info.sessionTitle === 'string' ? info.sessionTitle : '') || '';
309
+ const fromEvent = readSessionTitleFromEvent(payload);
310
+ let sessionTitle = trimmedText(payload?.properties?.sessionTitle)
311
+ || trimmedText(payload?.properties?.session?.title)
312
+ || trimmedText(info.sessionTitle)
313
+ || (fromEvent && (!sessionId || fromEvent.sessionId === sessionId) ? fromEvent.title : '');
231
314
 
232
315
  if (!sessionTitle && sessionId) {
233
316
  const cached = getCachedSessionTitle(sessionId);
@@ -238,25 +321,34 @@ export const createNotificationTemplateRuntime = (deps) => {
238
321
 
239
322
  let sessionInfo = null;
240
323
  if (!sessionTitle && sessionId) {
241
- sessionInfo = await fetchSessionInfo(sessionId);
242
- if (sessionInfo && typeof sessionInfo.title === 'string') {
243
- sessionTitle = sessionInfo.title;
324
+ sessionInfo = await fetchSessionInfo(sessionId, readNotificationDirectory(payload));
325
+ const fetchedTitle = trimmedText(sessionInfo?.title);
326
+ if (fetchedTitle) {
327
+ sessionTitle = fetchedTitle;
244
328
  cacheSessionTitle(sessionId, sessionTitle);
245
329
  }
246
330
  }
247
331
 
332
+ const eventAgent = trimmedText(info.agent) || trimmedText(info.mode);
333
+ const eventModel = trimmedText(info.modelID)
334
+ || trimmedText(info.model?.modelID)
335
+ || trimmedText(info.model?.id);
336
+ if ((!eventAgent || !eventModel) && sessionId && !sessionInfo) {
337
+ sessionInfo = await fetchSessionInfo(sessionId, readNotificationDirectory(payload));
338
+ }
339
+
248
340
  const agentName = (() => {
249
- const mode = typeof info.agent === 'string' && info.agent.trim().length > 0
250
- ? info.agent.trim()
251
- : (typeof info.mode === 'string' ? info.mode.trim() : '');
341
+ const mode = eventAgent || trimmedText(sessionInfo?.agent) || trimmedText(sessionInfo?.mode);
252
342
  if (!mode) return 'Agent';
253
343
  return mode.split(/[-_\s]+/).filter(Boolean)
254
344
  .map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join(' ');
255
345
  })();
256
346
 
257
347
  const modelName = (() => {
258
- const raw = typeof info.modelID === 'string' ? info.modelID.trim()
259
- : (typeof info.model?.modelID === 'string' ? info.model.modelID.trim() : '');
348
+ const raw = eventModel
349
+ || trimmedText(sessionInfo?.model?.modelID)
350
+ || trimmedText(sessionInfo?.model?.id)
351
+ || trimmedText(sessionInfo?.modelID);
260
352
  if (!raw) return 'Assistant';
261
353
  return raw.split(/[-_]+/).filter(Boolean)
262
354
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');