@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.5

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 (178) hide show
  1. package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-BBmQXYDq.js} +1 -1
  2. package/dist/assets/AssistantView-BG8hK4y9.js +3 -0
  3. package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-CN0qWNU5.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
  5. package/dist/assets/Icon-B9kZgYZn.js +1 -0
  6. package/dist/assets/{MarkdownRenderer-CJHSsWin.js → MarkdownRenderer-Ds61--LT.js} +2 -2
  7. package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-DOMmiOH0.js} +1 -1
  8. package/dist/assets/{MarkstreamRendererImpl-CSnbQMu3.js → MarkstreamRendererImpl-F7MGg5ER.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DcmVOjkw.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-C1Fc4ErB.js} +2 -2
  11. package/dist/assets/MobileSurface-BA7eS5Yd.js +2 -0
  12. package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-BoTnH6Yp.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BxItVnnO.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-B4e0QD05.js} +1 -1
  15. package/dist/assets/SettingsView-CjesWNLw.js +25 -0
  16. package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CReMbg-w.js} +1 -1
  17. package/dist/assets/TerminalView-C64-QYOG.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-CJu8DNrt.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-4MR0F7Jz.js → appThemeRegistry-CF4lIxNt.js} +1 -1
  20. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  21. package/dist/assets/{button-DKuz3L8l.js → button-CwXyux0a.js} +1 -1
  22. package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
  23. package/dist/assets/context-menu-DHrRifPE.js +1 -0
  24. package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-JGFqhlaM.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-DDlVKyOE.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  26. package/dist/assets/{diagram-D7T8HQgj.js → diagram-BwnPNKWt.js} +1 -1
  27. package/dist/assets/{dialog-DQYl3W_o.js → dialog-Dp0lQv5K.js} +1 -1
  28. package/dist/assets/es-DicJ8lKD.js +15 -0
  29. package/dist/assets/{fileMentionTouchSelection-BZ6ovBYH.js → fileMentionTouchSelection-DPZRxNa2.js} +3 -4
  30. package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DN6gbLhM.js} +1 -1
  31. package/dist/assets/fr-lhgVcBW_.js +13 -0
  32. package/dist/assets/{gitApi-CM42qUed.js → gitApi-D8R7ic-T.js} +3 -3
  33. package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BMztRu0c.js} +1 -1
  34. package/dist/assets/{input-CzQy8UhV.js → input-Biw8aNzR.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DdwCD4S2.js +1 -0
  36. package/dist/assets/ja-B9EZwOcw.js +15 -0
  37. package/dist/assets/ko-Dxk-uJRb.js +15 -0
  38. package/dist/assets/main-BhlwwJ2c.js +169 -0
  39. package/dist/assets/main-D2BzpnUt.css +1 -0
  40. package/dist/assets/main-DSPCkT74.js +2 -0
  41. package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-BOOrGCuG.js} +1 -1
  42. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  43. package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-CE9tk-Jz.js} +1 -1
  44. package/dist/assets/miniChat-Ckk9ABql.js +2 -0
  45. package/dist/assets/mobile-cLMRZqI9.js +2 -0
  46. package/dist/assets/multirun-DVnFRb1V.js +4 -0
  47. package/dist/assets/persistence-D0y7226-.js +3 -0
  48. package/dist/assets/pl-CFhwWLJj.js +15 -0
  49. package/dist/assets/platform-D_c-T5nO.js +6 -0
  50. package/dist/assets/projectMeta-CSQgHequ.js +3 -0
  51. package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
  52. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  53. package/dist/assets/renderElectronMiniChatApp-gGFkwoL_.js +2 -0
  54. package/dist/assets/renderMobileApp-B2w50kAc.js +28 -0
  55. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  56. package/dist/assets/{runtimeConfig-BIGBRGQI.js → runtimeConfig-BHH1hJDd.js} +1 -1
  57. package/dist/assets/runtimeEndpointReset-5uJ4sJk0.js +149 -0
  58. package/dist/assets/{sessionChangeSummary-eIbpSfrO.js → sessionChangeSummary-spf_atO8.js} +2 -2
  59. package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-BGjjwiCq.js} +1 -1
  60. package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
  61. package/dist/assets/src-BjEyU6if.css +2 -0
  62. package/dist/assets/store-DmowK-GO.js +15 -0
  63. package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-Dhqqp4xi.js} +1 -1
  64. package/dist/assets/textarea-QxsmcBGj.js +1 -0
  65. package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-CpMLl5_k.js} +1 -1
  66. package/dist/assets/uk-ChchBzy6.js +15 -0
  67. package/dist/assets/{url-CDHnjlsm.js → url-CGbJbiJq.js} +1 -1
  68. package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-B1h7ab_J.js} +55 -50
  69. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  70. package/dist/assets/useDesktopWindowControlsLayout-Cxsx84NO.js +1 -0
  71. package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-DsqhqsJR.js} +1 -1
  72. package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-Xw1vSkmC.js} +1 -1
  73. package/dist/assets/useMobileNavigationStore-B1axS4g-.js +414 -0
  74. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  75. package/dist/assets/useSessionAutoCleanup-B5FTG_qf.js +2 -0
  76. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  77. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  78. package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-DC_Bc-RV.js} +1 -1
  79. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  80. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  81. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  84. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  85. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  86. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  87. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  88. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  89. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  90. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  91. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-B8parU1Y.js} +1 -1
  92. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  93. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  94. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  95. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  96. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
  97. package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
  98. package/dist/assets/zh-TW-CZeTm7kd.js +15 -0
  99. package/dist/index.html +14 -14
  100. package/dist/mini-chat.html +14 -14
  101. package/dist/mobile.html +14 -14
  102. package/package.json +3 -2
  103. package/server/index.js +38 -7
  104. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  105. package/server/lib/browser-provider/contract.js +312 -0
  106. package/server/lib/browser-provider/host.js +421 -0
  107. package/server/lib/browser-provider/host.test.js +304 -0
  108. package/server/lib/browser-provider/routes.js +122 -0
  109. package/server/lib/browser-provider/routes.test.js +120 -0
  110. package/server/lib/browser-provider/selection-store.js +43 -0
  111. package/server/lib/browser-provider/selection-store.test.js +35 -0
  112. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  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/notifications/DOCUMENTATION.md +3 -1
  116. package/server/lib/notifications/runtime.js +128 -31
  117. package/server/lib/notifications/runtime.test.js +103 -0
  118. package/server/lib/notifications/template-runtime.js +130 -38
  119. package/server/lib/notifications/template-runtime.test.js +96 -0
  120. package/server/lib/opencode/DOCUMENTATION.md +8 -0
  121. package/server/lib/opencode/config-entity-routes.js +6 -1
  122. package/server/lib/opencode/config-entity-routes.test.js +25 -0
  123. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  124. package/server/lib/opencode/feature-routes-runtime.js +10 -0
  125. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  126. package/server/lib/opencode/proxy.js +6 -1
  127. package/server/lib/opencode/proxy.test.js +18 -3
  128. package/server/lib/opencode/routes.js +30 -0
  129. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  130. package/server/lib/opencode/upgrade-screen.js +299 -0
  131. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  132. package/server/lib/session-metadata/DOCUMENTATION.md +27 -17
  133. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  134. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  135. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  136. package/server/lib/session-metadata/routes.js +5 -1
  137. package/server/lib/session-metadata/session-archive.js +1 -1
  138. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  139. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  140. package/server/lib/session-metadata/session-projection.js +26 -2
  141. package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
  142. package/dist/assets/Icon-BiEQEcih.js +0 -1
  143. package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
  144. package/dist/assets/SettingsView-BRxELjow.js +0 -25
  145. package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
  146. package/dist/assets/collapsible-DXbDynN4.js +0 -3
  147. package/dist/assets/context-menu-CqolsLCm.js +0 -1
  148. package/dist/assets/es-CN3E_zXj.js +0 -15
  149. package/dist/assets/fr-9zZvQZD2.js +0 -13
  150. package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
  151. package/dist/assets/ja-Dnjk_82N.js +0 -15
  152. package/dist/assets/ko-BxCEE99C.js +0 -15
  153. package/dist/assets/main-BILgBI3B.js +0 -169
  154. package/dist/assets/main-BeveiW6F.js +0 -2
  155. package/dist/assets/main-qSeeJRW1.css +0 -1
  156. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  157. package/dist/assets/miniChat-BgrzM3u7.js +0 -2
  158. package/dist/assets/mobile-DtYoQE8C.js +0 -2
  159. package/dist/assets/multirun-jy8dEI96.js +0 -4
  160. package/dist/assets/persistence-C9LQpiOS.js +0 -3
  161. package/dist/assets/pl-CNNsWnPM.js +0 -15
  162. package/dist/assets/platform-BQghHRfp.js +0 -6
  163. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  164. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
  165. package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
  166. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  167. package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
  168. package/dist/assets/src-CLN7Z-zx.css +0 -2
  169. package/dist/assets/store-DLCiZaA6.js +0 -15
  170. package/dist/assets/textarea-CakKvqFw.js +0 -1
  171. package/dist/assets/uk-rwQrxeYM.js +0 -15
  172. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
  173. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
  174. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
  175. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  176. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  177. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  178. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
@@ -13,6 +13,21 @@ import {
13
13
  } from './reasoning-projection.js';
14
14
 
15
15
  describe('shouldIncludeReasoning / query helpers', () => {
16
+ it('projects v2 list/context and exact snapshots without dropping rows or cursor', () => {
17
+ const row = { id: 'm', type: 'assistant', tokens: { reasoning: 12 }, content: [
18
+ { type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
19
+ ] };
20
+ const payload = { data: [row], cursor: { next: 'older' } };
21
+ const projected = projectMessagesPayloadForReasoning(payload, false);
22
+ expect(projected).toEqual({ data: [{ ...row, content: [row.content[1]] }], cursor: payload.cursor });
23
+ expect(projectMessagesPayloadForReasoning(row, false)).toEqual(projected.data[0]);
24
+ expect(projectMessagesPayloadForReasoning(payload, true)).toBe(payload);
25
+ expect(row.content).toHaveLength(2);
26
+ const filter = createReasoningOutboundFilter();
27
+ expect(filter.projectEvent({ type: 'session.reasoning.delta.1', data: { delta: 'hidden' } })).toBeNull();
28
+ const text = { type: 'session.text.delta', data: { delta: 'visible' } };
29
+ expect(filter.projectEvent(text)).toBe(text);
30
+ });
16
31
  it('only treats the strict string false as disabled', () => {
17
32
  expect(shouldIncludeReasoning(undefined)).toBe(true);
18
33
  expect(shouldIncludeReasoning(null)).toBe(true);
@@ -107,7 +107,9 @@ This module provides notification message preparation utilities for the web serv
107
107
  - `extractLastMessageText(payload, maxLength?)`
108
108
  - `fetchLastAssistantMessageText(sessionId, messageId, maxLength?)`
109
109
  - `maybeCacheSessionInfoFromEvent(payload)`
110
- - `buildTemplateVariables(payload, sessionId)`
110
+ - `buildTemplateVariables(payload, sessionId)` — `session_name` is the session title. OpenCode 2 returns that title on `GET /api/session/:id` as `{ data: { title } }` and on `session.created` / `session.renamed` as `data.title`. Legacy `properties.info.title` still counts. An empty title stays empty so push can fall back to the localized "Session" label. `agent_name` / `model_name` fall back to that same session record when the triggering event has none. `fetchLastAssistantMessageText` accepts a legacy message array and an OpenCode 2 `{ data }` page (`order=desc`).
111
+ - Question prompts come from legacy `question.asked` or OpenCode 2 `form.created` (`data.form`). Permission prompts read `permission.asked` from `properties` or `data` (`action`, `message`, `id`). `form.replied` / `form.cancelled` cancel a pending question notification the same way `permission.replied` cancels a permission notification.
112
+ - iOS APNs and Android FCM share this payload: native banners stay scenario title + session name. Desktop, in-app, and web-push use the resolved template title and body.
111
113
  - `getCachedZenModels()`
112
114
 
113
115
  ## Constants
@@ -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 {
@@ -417,20 +487,31 @@ export const createNotificationTriggerRuntime = (deps) => {
417
487
  const reason = payload.properties?.reason ?? payload.data?.reason;
418
488
  if (reason === 'shutdown') return;
419
489
  }
420
- if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
421
- const error = payload.properties?.error;
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;
422
502
  const errorText = typeof error?.message === 'string'
423
503
  ? error.message
424
504
  : typeof error === 'string' ? error : '';
505
+ const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
425
506
  await maybeSendPushForTrigger({
426
507
  ...payload,
427
508
  type: 'message.updated',
428
509
  properties: {
429
- ...payload.properties,
510
+ ...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
430
511
  info: {
431
512
  sessionID: sessionId,
432
513
  role: 'assistant',
433
- finish: payload.type === 'session.error' ? 'error' : 'stop',
514
+ finish: failed ? 'error' : 'stop',
434
515
  ...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
435
516
  },
436
517
  },
@@ -492,12 +573,12 @@ export const createNotificationTriggerRuntime = (deps) => {
492
573
  const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
493
574
 
494
575
  const variables = await buildTemplateVariables(payload, sessionId);
495
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
576
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
496
577
 
497
578
  const messageId = info?.id;
498
579
  let lastMessage = extractLastMessageText(payload);
499
580
  if (!lastMessage) {
500
- lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
581
+ lastMessage = await fetchLastAssistantMessageText(sessionId, messageId, undefined, notificationDirectory);
501
582
  }
502
583
 
503
584
  variables.last_message = await prepareNotificationLastMessage({
@@ -548,7 +629,21 @@ export const createNotificationTriggerRuntime = (deps) => {
548
629
  return;
549
630
  }
550
631
 
551
- 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) {
552
647
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
553
648
  return;
554
649
  }
@@ -569,9 +664,9 @@ export const createNotificationTriggerRuntime = (deps) => {
569
664
  return;
570
665
  }
571
666
 
572
- const firstQuestion = payload.properties?.questions?.[0];
573
- const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
574
- const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
667
+ const prompt = readNotificationEventFields(payload);
668
+ const header = prompt.header;
669
+ const questionText = prompt.questionText;
575
670
 
576
671
  let title = 'Needs your answer';
577
672
  let body = '';
@@ -579,7 +674,7 @@ export const createNotificationTriggerRuntime = (deps) => {
579
674
 
580
675
  try {
581
676
  const variables = await buildTemplateVariables(payload, sessionId);
582
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
677
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
583
678
  variables.last_message = questionText || header || '';
584
679
 
585
680
  const templates = settings.notificationTemplates || {};
@@ -630,8 +725,8 @@ export const createNotificationTriggerRuntime = (deps) => {
630
725
  }
631
726
 
632
727
  if (payload.type === 'permission.replied' && sessionId) {
633
- const requestId = payload.properties?.requestID ?? payload.properties?.requestId ?? payload.properties?.id;
634
- const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
728
+ const requestId = readNotificationEventFields(payload).requestId;
729
+ const requestKey = requestId ? `${sessionId}:${requestId}` : null;
635
730
  const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
636
731
  if (!pendingNotification) {
637
732
  return;
@@ -651,9 +746,10 @@ export const createNotificationTriggerRuntime = (deps) => {
651
746
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
652
747
  return;
653
748
  }
654
- const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
655
- const permission = payload.properties?.permission;
656
- 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;
657
753
  if (requestKey && notifiedPermissionRequests.has(requestKey)) {
658
754
  return;
659
755
  }
@@ -691,11 +787,12 @@ export const createNotificationTriggerRuntime = (deps) => {
691
787
  return;
692
788
  }
693
789
 
694
- const sessionTitle = payload.properties?.sessionTitle;
790
+ const sessionTitle = payload.properties?.sessionTitle ?? payload.data?.sessionTitle;
695
791
  const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
792
+ const detail = permissionFields.message;
696
793
  const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
697
794
  ? sessionTitle.trim()
698
- : permissionText || 'Agent is waiting for your approval';
795
+ : detail || permissionText || 'Agent is waiting for your approval';
699
796
 
700
797
  let title = 'Needs permission';
701
798
  let body = '';
@@ -703,7 +800,7 @@ export const createNotificationTriggerRuntime = (deps) => {
703
800
 
704
801
  try {
705
802
  const variables = await buildTemplateVariables(payload, sessionId);
706
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
803
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
707
804
  variables.last_message = fallbackMessage;
708
805
 
709
806
  const templates = settings.notificationTemplates || {};
@@ -771,8 +868,8 @@ export const createNotificationTriggerRuntime = (deps) => {
771
868
  signal: AbortSignal.timeout(2000),
772
869
  });
773
870
  if (response.ok) {
774
- const session = await response.json().catch(() => null);
775
- if (typeof session?.title === 'string') sessionName = session.title.trim();
871
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
872
+ sessionName = trimmedText(session?.title);
776
873
  }
777
874
  } catch {
778
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