@openchambery/web 2.0.0-beta.4 → 2.0.0-beta.6

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 (172) hide show
  1. package/dist/assets/{AgentAvatar-Q1SmFRx2.js → AgentAvatar-BBmQXYDq.js} +1 -1
  2. package/dist/assets/AssistantView-BpvbpZBC.js +3 -0
  3. package/dist/assets/{DiagramView-BW92schO.js → DiagramView-UwKDr8HS.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-BZmwl-zE.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
  5. package/dist/assets/Icon-B9kZgYZn.js +1 -0
  6. package/dist/assets/{MarkdownRenderer-Bv05iDZZ.js → MarkdownRenderer-C8tDsO5F.js} +2 -2
  7. package/dist/assets/{MarkdownRendererImpl-C81xqYib.js → MarkdownRendererImpl-DRZ1iTbg.js} +1 -1
  8. package/dist/assets/MarkstreamRendererImpl-fU6uhDTr.js +3 -0
  9. package/dist/assets/{MobileDetailNavigation-BMgIu7zc.js → MobileDetailNavigation-MXUDe0nG.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-BOXGNHmC.js → MobileShareBridge-C7x4jOGb.js} +2 -2
  11. package/dist/assets/MobileSurface-BQSmXYJ_.js +2 -0
  12. package/dist/assets/{MultiRunWindow-B5At2Z95.js → MultiRunWindow-ClfxxgP5.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-bsPLgz4r.js → OnboardingScreen-BxItVnnO.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-4wp4migh.js → OverlayScrollbar-B4e0QD05.js} +1 -1
  15. package/dist/assets/SettingsView-DtWgey4y.js +25 -0
  16. package/dist/assets/{SettingsWindow-CMtX2bMl.js → SettingsWindow-Du8y1EI8.js} +1 -1
  17. package/dist/assets/TerminalView-DL9qkQDj.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-uH1ZZDSS.js → ToolOutputDialog-BXawDmmf.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-DK7q2juD.js → appThemeRegistry-DsSahEsq.js} +1 -1
  20. package/dist/assets/{button-CcNJUxUd.js → button-CwXyux0a.js} +1 -1
  21. package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
  22. package/dist/assets/context-menu-DHrRifPE.js +1 -0
  23. package/dist/assets/{desktopHosts-C6HrP0jq.js → desktopHosts-JGFqhlaM.js} +1 -1
  24. package/dist/assets/{desktopRelayRestore-DdVBp45U.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  25. package/dist/assets/{diagram-CxTs9hCQ.js → diagram-BwnPNKWt.js} +1 -1
  26. package/dist/assets/{dialog-KMVL9QKF.js → dialog-Dp0lQv5K.js} +1 -1
  27. package/dist/assets/es-DicJ8lKD.js +15 -0
  28. package/dist/assets/fileMentionTouchSelection-PZQrywzZ.js +3 -0
  29. package/dist/assets/{fileQueries-CjdBRy8y.js → fileQueries-DN6gbLhM.js} +1 -1
  30. package/dist/assets/fr-lhgVcBW_.js +13 -0
  31. package/dist/assets/{gitApi-yuWg9BN4.js → gitApi-C1oGENfw.js} +4 -4
  32. package/dist/assets/{imageSource-9vQBq3zp.js → imageSource-BMztRu0c.js} +1 -1
  33. package/dist/assets/{input-CrIvk3HZ.js → input-Biw8aNzR.js} +1 -1
  34. package/dist/assets/insertionBoundaries-BmQh-tke.js +1 -0
  35. package/dist/assets/ja-B9EZwOcw.js +15 -0
  36. package/dist/assets/ko-Dxk-uJRb.js +15 -0
  37. package/dist/assets/main-BnyE5H4i.js +169 -0
  38. package/dist/assets/main-D2BzpnUt.css +1 -0
  39. package/dist/assets/main-DOgXHO44.js +2 -0
  40. package/dist/assets/{markdownHeightCache-BmXVY_gc.js → markdownHeightCache-BOOrGCuG.js} +1 -1
  41. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  42. package/dist/assets/{mcpQueries-C7jknHV8.js → mcpQueries-D9Dyzcve.js} +1 -1
  43. package/dist/assets/miniChat-C7FecHIX.js +2 -0
  44. package/dist/assets/mobile-_zrfbam4.js +2 -0
  45. package/dist/assets/multirun-BLFbv4I9.js +4 -0
  46. package/dist/assets/persistence-D0y7226-.js +3 -0
  47. package/dist/assets/pl-CFhwWLJj.js +15 -0
  48. package/dist/assets/platform-D_c-T5nO.js +6 -0
  49. package/dist/assets/projectMeta-BCdCRuYS.js +3 -0
  50. package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
  51. package/dist/assets/renderElectronMiniChatApp-QQq7dj-0.js +2 -0
  52. package/dist/assets/renderMobileApp-BVIkeTKi.js +28 -0
  53. package/dist/assets/{runtimeConfig-BEANkvOd.js → runtimeConfig-S9D4fVI5.js} +1 -1
  54. package/dist/assets/runtimeEndpointReset-B6eE14f9.js +149 -0
  55. package/dist/assets/sessionChangeSummary-DLo3Iv_g.js +2 -0
  56. package/dist/assets/{sessionLookup-34XFmP9C.js → sessionLookup-BXXTw54z.js} +1 -1
  57. package/dist/assets/{sortable-tabs-strip-DyEV0Heg.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
  58. package/dist/assets/src-lS1keDVV.css +2 -0
  59. package/dist/assets/store-DmowK-GO.js +15 -0
  60. package/dist/assets/{terminalApi-CXaBEoa_.js → terminalApi-Dhqqp4xi.js} +1 -1
  61. package/dist/assets/{textarea-BBOlRI9O.js → textarea-QxsmcBGj.js} +1 -1
  62. package/dist/assets/{tooltip-CPI4fr1a.js → tooltip-CpMLl5_k.js} +1 -1
  63. package/dist/assets/uk-ChchBzy6.js +15 -0
  64. package/dist/assets/{url-DRDQXjyX.js → url-CGbJbiJq.js} +1 -1
  65. package/dist/assets/{useAppFontEffects-CiDe42I5.js → useAppFontEffects-BDs0O2qK.js} +56 -51
  66. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  67. package/dist/assets/useDesktopWindowControlsLayout-ojoBEglc.js +1 -0
  68. package/dist/assets/{useEffectiveDirectory-C_CouL5v.js → useEffectiveDirectory-B_nHyDMJ.js} +1 -1
  69. package/dist/assets/{useFontPreferences-B0oUZAiB.js → useFontPreferences-Xw1vSkmC.js} +1 -1
  70. package/dist/assets/useMobileNavigationStore-D_96D661.js +414 -0
  71. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  72. package/dist/assets/useSessionAutoCleanup-Dw34f-EW.js +2 -0
  73. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  74. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  75. package/dist/assets/{useWorkerHighlightedLines-CPNYz6Wf.js → useWorkerHighlightedLines-CNlfP7nA.js} +1 -1
  76. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  77. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  78. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  79. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  80. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  81. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  82. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  83. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  84. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  85. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  86. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  87. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  88. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  89. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  90. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  91. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  92. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  93. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
  94. package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
  95. package/dist/assets/zh-TW-CZeTm7kd.js +15 -0
  96. package/dist/index.html +13 -13
  97. package/dist/mini-chat.html +13 -13
  98. package/dist/mobile.html +13 -13
  99. package/package.json +3 -2
  100. package/server/index.js +38 -7
  101. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  102. package/server/lib/browser-provider/contract.js +312 -0
  103. package/server/lib/browser-provider/host.js +421 -0
  104. package/server/lib/browser-provider/host.test.js +304 -0
  105. package/server/lib/browser-provider/routes.js +122 -0
  106. package/server/lib/browser-provider/routes.test.js +120 -0
  107. package/server/lib/browser-provider/selection-store.js +43 -0
  108. package/server/lib/browser-provider/selection-store.test.js +35 -0
  109. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  110. package/server/lib/notifications/runtime.js +110 -26
  111. package/server/lib/notifications/runtime.test.js +92 -0
  112. package/server/lib/notifications/template-runtime.js +130 -38
  113. package/server/lib/notifications/template-runtime.test.js +96 -0
  114. package/server/lib/opencode/DOCUMENTATION.md +4 -0
  115. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  116. package/server/lib/opencode/feature-routes-runtime.js +10 -0
  117. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  118. package/server/lib/opencode/proxy.js +3 -1
  119. package/server/lib/opencode/proxy.test.js +3 -3
  120. package/server/lib/opencode/routes.js +30 -0
  121. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  122. package/server/lib/opencode/upgrade-screen.js +299 -0
  123. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  124. package/server/lib/session-metadata/DOCUMENTATION.md +27 -17
  125. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  126. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  127. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  128. package/server/lib/session-metadata/routes.js +5 -1
  129. package/server/lib/session-metadata/session-archive.js +1 -1
  130. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  131. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  132. package/server/lib/session-metadata/session-projection.js +26 -2
  133. package/server/lib/session-title/runtime.js +2 -2
  134. package/dist/assets/AssistantView-CwXkiCAd.js +0 -3
  135. package/dist/assets/Icon-AQPvxMiU.js +0 -1
  136. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +0 -3
  137. package/dist/assets/MobileSurface-xuc_z0_G.js +0 -2
  138. package/dist/assets/SettingsView-CJ6tcH5_.js +0 -25
  139. package/dist/assets/TerminalView-Dvr-ysTS.js +0 -8
  140. package/dist/assets/collapsible-ZmZ6rLWs.js +0 -3
  141. package/dist/assets/context-menu-B2LXJgXL.js +0 -1
  142. package/dist/assets/es-CN3E_zXj.js +0 -15
  143. package/dist/assets/fileMentionTouchSelection-7RIwlpkB.js +0 -4
  144. package/dist/assets/fr-9zZvQZD2.js +0 -13
  145. package/dist/assets/insertionBoundaries-BiUCSkx-.js +0 -1
  146. package/dist/assets/ja-Dnjk_82N.js +0 -15
  147. package/dist/assets/ko-BxCEE99C.js +0 -15
  148. package/dist/assets/main-C9mIBo77.js +0 -2
  149. package/dist/assets/main-DiL6w6KF.js +0 -169
  150. package/dist/assets/main-qSeeJRW1.css +0 -1
  151. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  152. package/dist/assets/miniChat-CwtUaElU.js +0 -2
  153. package/dist/assets/mobile-DB-6--ws.js +0 -2
  154. package/dist/assets/multirun-TAsxFI8e.js +0 -4
  155. package/dist/assets/persistence-56kq8Z4V.js +0 -3
  156. package/dist/assets/pl-CNNsWnPM.js +0 -15
  157. package/dist/assets/platform-C13fMVEs.js +0 -6
  158. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  159. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +0 -2
  160. package/dist/assets/renderMobileApp-CGMMqbsO.js +0 -28
  161. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +0 -147
  162. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +0 -2
  163. package/dist/assets/src-ubCWXZhR.css +0 -2
  164. package/dist/assets/store-DLCiZaA6.js +0 -15
  165. package/dist/assets/uk-rwQrxeYM.js +0 -15
  166. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +0 -1
  167. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +0 -414
  168. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +0 -2
  169. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  170. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  171. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  172. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
@@ -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
  };
@@ -350,11 +388,20 @@ export const createNotificationTriggerRuntime = (deps) => {
350
388
  ? payload.properties
351
389
  : (payload.data && typeof payload.data === 'object' ? payload.data : null);
352
390
  const info = props?.info;
391
+ const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
392
+ ? payload.data
393
+ : null;
353
394
  const sessionId =
354
395
  info?.sessionID ??
355
396
  info?.sessionId ??
356
397
  props?.sessionID ??
357
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 ??
358
405
  props?.session ??
359
406
  null;
360
407
  return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
@@ -362,11 +409,32 @@ export const createNotificationTriggerRuntime = (deps) => {
362
409
 
363
410
  const extractDirectoryFromPayload = (payload) => {
364
411
  if (!payload || typeof payload !== 'object') return undefined;
365
- const props = payload.properties;
366
- const directory = props?.directory ?? props?.info?.directory;
367
- if (typeof directory !== 'string') return undefined;
368
- const trimmed = directory.trim();
369
- 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);
370
438
  };
371
439
 
372
440
  // A session with an ACTIVE goal suppresses per-turn ready notifications;
@@ -383,7 +451,7 @@ export const createNotificationTriggerRuntime = (deps) => {
383
451
  signal: AbortSignal.timeout(2000),
384
452
  });
385
453
  if (!response.ok) return false;
386
- const session = await response.json().catch(() => null);
454
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
387
455
  const goal = session?.metadata?.openchamber?.goal;
388
456
  return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
389
457
  } catch {
@@ -505,12 +573,12 @@ export const createNotificationTriggerRuntime = (deps) => {
505
573
  const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
506
574
 
507
575
  const variables = await buildTemplateVariables(payload, sessionId);
508
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
576
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
509
577
 
510
578
  const messageId = info?.id;
511
579
  let lastMessage = extractLastMessageText(payload);
512
580
  if (!lastMessage) {
513
- lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
581
+ lastMessage = await fetchLastAssistantMessageText(sessionId, messageId, undefined, notificationDirectory);
514
582
  }
515
583
 
516
584
  variables.last_message = await prepareNotificationLastMessage({
@@ -561,7 +629,21 @@ export const createNotificationTriggerRuntime = (deps) => {
561
629
  return;
562
630
  }
563
631
 
564
- 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) {
565
647
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
566
648
  return;
567
649
  }
@@ -582,9 +664,9 @@ export const createNotificationTriggerRuntime = (deps) => {
582
664
  return;
583
665
  }
584
666
 
585
- const firstQuestion = payload.properties?.questions?.[0];
586
- const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
587
- const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
667
+ const prompt = readNotificationEventFields(payload);
668
+ const header = prompt.header;
669
+ const questionText = prompt.questionText;
588
670
 
589
671
  let title = 'Needs your answer';
590
672
  let body = '';
@@ -592,7 +674,7 @@ export const createNotificationTriggerRuntime = (deps) => {
592
674
 
593
675
  try {
594
676
  const variables = await buildTemplateVariables(payload, sessionId);
595
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
677
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
596
678
  variables.last_message = questionText || header || '';
597
679
 
598
680
  const templates = settings.notificationTemplates || {};
@@ -643,8 +725,8 @@ export const createNotificationTriggerRuntime = (deps) => {
643
725
  }
644
726
 
645
727
  if (payload.type === 'permission.replied' && sessionId) {
646
- const requestId = payload.properties?.requestID ?? payload.properties?.requestId ?? payload.properties?.id;
647
- const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
728
+ const requestId = readNotificationEventFields(payload).requestId;
729
+ const requestKey = requestId ? `${sessionId}:${requestId}` : null;
648
730
  const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
649
731
  if (!pendingNotification) {
650
732
  return;
@@ -664,9 +746,10 @@ export const createNotificationTriggerRuntime = (deps) => {
664
746
  if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
665
747
  return;
666
748
  }
667
- const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
668
- const permission = payload.properties?.permission;
669
- 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;
670
753
  if (requestKey && notifiedPermissionRequests.has(requestKey)) {
671
754
  return;
672
755
  }
@@ -704,11 +787,12 @@ export const createNotificationTriggerRuntime = (deps) => {
704
787
  return;
705
788
  }
706
789
 
707
- const sessionTitle = payload.properties?.sessionTitle;
790
+ const sessionTitle = payload.properties?.sessionTitle ?? payload.data?.sessionTitle;
708
791
  const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
792
+ const detail = permissionFields.message;
709
793
  const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
710
794
  ? sessionTitle.trim()
711
- : permissionText || 'Agent is waiting for your approval';
795
+ : detail || permissionText || 'Agent is waiting for your approval';
712
796
 
713
797
  let title = 'Needs permission';
714
798
  let body = '';
@@ -716,7 +800,7 @@ export const createNotificationTriggerRuntime = (deps) => {
716
800
 
717
801
  try {
718
802
  const variables = await buildTemplateVariables(payload, sessionId);
719
- sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
803
+ sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
720
804
  variables.last_message = fallbackMessage;
721
805
 
722
806
  const templates = settings.notificationTemplates || {};
@@ -784,8 +868,8 @@ export const createNotificationTriggerRuntime = (deps) => {
784
868
  signal: AbortSignal.timeout(2000),
785
869
  });
786
870
  if (response.ok) {
787
- const session = await response.json().catch(() => null);
788
- if (typeof session?.title === 'string') sessionName = session.title.trim();
871
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
872
+ sessionName = trimmedText(session?.title);
789
873
  }
790
874
  } catch {
791
875
  // Session name is presentation sugar for the mobile push — never block on it.
@@ -626,6 +626,97 @@ describe('notification event toggles', () => {
626
626
  expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
627
627
  });
628
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
+
629
720
  it('still sends goal settle push by default', async () => {
630
721
  vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ id: 'ses_goal', title: 'Goal run' })));
631
722
  const { runtime, sendPushToAllUiSessions } = createRuntime();
@@ -637,6 +728,7 @@ describe('notification event toggles', () => {
637
728
  });
638
729
  expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
639
730
  expect(sendPushToAllUiSessions.mock.calls[0][0]).toMatchObject({ tag: 'goal-ses_goal' });
731
+ expect(sendPushToAllUiSessions.mock.calls[0][0].data).toMatchObject({ sessionName: 'Goal run' });
640
732
  });
641
733
  });
642
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(' ');