conductor-oss 0.19.2 → 0.20.1

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 (217) hide show
  1. package/package.json +5 -5
  2. package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
  3. package/web/.next/standalone/packages/web/.next/build-manifest.json +4 -4
  4. package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
  5. package/web/.next/standalone/packages/web/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  7. package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  8. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/build-manifest.json +2 -2
  14. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
  15. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
  18. package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +4 -4
  19. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  20. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +4 -4
  22. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -1
  26. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +1 -1
  27. package/web/.next/standalone/packages/web/.next/server/app/api/app-update/route.js.nft.json +1 -1
  28. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -1
  29. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js.nft.json +1 -1
  30. package/web/.next/standalone/packages/web/.next/server/app/api/boards/comments/route.js.nft.json +1 -1
  31. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -1
  32. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/open/route.js.nft.json +1 -1
  34. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -1
  35. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -1
  36. package/web/.next/standalone/packages/web/.next/server/app/api/executor/health/route.js.nft.json +1 -1
  37. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -1
  38. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/pick-directory/route.js.nft.json +1 -1
  39. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -1
  40. package/web/.next/standalone/packages/web/.next/server/app/api/github/webhook/route.js.nft.json +1 -1
  41. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -1
  42. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -1
  43. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -1
  44. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -1
  45. package/web/.next/standalone/packages/web/.next/server/app/api/remote-access/route.js.nft.json +1 -1
  46. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/[id]/route.js.nft.json +1 -1
  47. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -1
  48. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -1
  49. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  50. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -1
  51. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -1
  52. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/route.js.nft.json +1 -1
  53. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -1
  54. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -1
  55. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/interrupt/route.js.nft.json +1 -1
  56. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  57. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -1
  58. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/dom/route.js.nft.json +1 -1
  59. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/route.js.nft.json +1 -1
  60. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/preview/screenshot/route.js.nft.json +1 -1
  61. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  62. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  63. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/snapshot/route.js.nft.json +1 -1
  64. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/token/route.js.nft.json +1 -1
  65. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/ttyd/route.js.nft.json +1 -1
  66. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/terminal/ttyd/ws/route.js.nft.json +1 -1
  67. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
  68. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -1
  69. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -1
  70. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  71. package/web/.next/standalone/packages/web/.next/server/app/page/build-manifest.json +2 -2
  72. package/web/.next/standalone/packages/web/.next/server/app/page/react-loadable-manifest.json +4 -4
  73. package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
  74. package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  75. package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/build-manifest.json +2 -2
  77. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/react-loadable-manifest.json +3 -3
  78. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
  79. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  80. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  81. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/build-manifest.json +2 -2
  82. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
  83. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
  84. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
  85. package/web/.next/standalone/packages/web/.next/server/app/unlock/page/build-manifest.json +2 -2
  86. package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
  87. package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
  88. package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
  89. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__53bcdbc9._.js +1 -1
  90. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__9507420a._.js +3 -0
  91. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__cb0aeb63._.js → [root-of-the-server]__b836223d._.js} +2 -2
  92. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__c405048c._.js +1 -1
  93. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__ce4b4158._.js → [root-of-the-server]__ec7a02cd._.js} +1 -1
  94. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
  95. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_3aee3cff._.js +3 -0
  96. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_4c8195c2._.js +3 -0
  97. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_532f707d._.js +1 -1
  98. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
  99. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
  100. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_903c3fe3._.js +1 -1
  101. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_a990354c._.js +1 -1
  102. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b9330c69._.js +1 -1
  103. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
  104. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
  105. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_152e81cd._.js +3 -0
  106. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_@clerk_nextjs_dist_esm_app-router_3eb7b454._.js → node_modules_@clerk_nextjs_dist_esm_app-router_2160d17f._.js} +2 -2
  107. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_e6b5eccd._.js +3 -0
  108. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@radix-ui_19d0f177._.js +3 -0
  109. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_b8d99fb4._.js → node_modules_@radix-ui_ef419f6b._.js} +2 -2
  110. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_core_dist_types_ba19386c.js +1 -1
  111. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_components_board_WorkspaceKanban_tsx_735b7999._.js +1 -1
  112. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_components_sessions_SessionPreview_tsx_ec32db81._.js +1 -1
  113. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_features_dashboard_DashboardClient_tsx_81ae42b0._.js +1 -1
  114. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_features_dashboard_components_DashboardDialogs_tsx_32d3d858._.js +1 -1
  115. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_hooks_5cb69d9a._.js +1 -1
  116. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_hooks_f05b02cd._.js +1 -1
  117. package/web/.next/standalone/packages/web/.next/server/middleware-build-manifest.js +2 -2
  118. package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
  119. package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
  120. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  121. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
  122. package/web/.next/standalone/packages/web/.next/static/chunks/131f2cf9c96f2724.js +1 -0
  123. package/web/.next/standalone/packages/web/.next/static/chunks/{96b41c96a34195c2.js → 437b46a75e1c92a7.js} +1 -1
  124. package/web/.next/standalone/packages/web/.next/static/chunks/450e4de887dcd142.js +1 -0
  125. package/web/.next/standalone/packages/web/.next/static/chunks/{81e3cea43881b17d.js → 4b3ac491753f4910.js} +1 -1
  126. package/web/.next/standalone/packages/web/.next/static/chunks/4f912a60e3753a1f.css +4 -0
  127. package/web/.next/standalone/packages/web/.next/static/chunks/581440483618ce47.js +1 -0
  128. package/web/.next/standalone/packages/web/.next/static/chunks/5c60a749d318d388.js +1 -0
  129. package/web/.next/standalone/packages/web/.next/static/chunks/5cb35e89898ff9e1.js +1 -0
  130. package/web/.next/standalone/packages/web/.next/static/chunks/9b1065a7ccd69a3e.js +1 -0
  131. package/web/.next/{static/chunks/355bedd41c2e9b0c.js → standalone/packages/web/.next/static/chunks/ae6e408453a5d1dc.js} +1 -1
  132. package/web/.next/standalone/packages/web/.next/static/chunks/b1c03c2c194b81b8.js +1 -0
  133. package/web/.next/standalone/packages/web/.next/static/chunks/b9c1238cc9302baa.js +1 -0
  134. package/web/.next/standalone/packages/web/.next/static/chunks/{378daaa8dbf5309c.js → c1f519b055b3c31e.js} +1 -1
  135. package/web/.next/standalone/packages/web/.next/static/chunks/c3443f28deeae499.js +1 -0
  136. package/web/.next/standalone/packages/web/.next/static/chunks/{4f9d3a3c14fb43da.js → c8e8ff2836531532.js} +1 -1
  137. package/web/.next/standalone/packages/web/.next/static/chunks/cb1a507b11d64bfb.js +1 -0
  138. package/web/.next/standalone/packages/web/.next/static/chunks/ce2f01a7e421d6fe.js +1 -0
  139. package/web/.next/standalone/packages/web/.next/static/chunks/{d934d0e74594a818.js → d27a0b78d8b5f352.js} +1 -1
  140. package/web/.next/standalone/packages/web/.next/static/chunks/{a70ab593d305d020.js → d460861f6cba32ab.js} +1 -1
  141. package/web/.next/standalone/packages/web/.next/static/chunks/dd6469b26407178b.js +1 -0
  142. package/web/.next/standalone/packages/web/.next/static/chunks/e59b8003fba5213d.js +1 -0
  143. package/web/.next/standalone/packages/web/.next/static/chunks/{459f77e3c65b47d6.js → fc68108d32aea368.js} +1 -1
  144. package/web/.next/standalone/packages/web/.next/static/chunks/{turbopack-9809b4096e86149a.js → turbopack-c76ead44073de602.js} +1 -1
  145. package/web/.next/standalone/packages/web/src/components/board/WorkspaceKanban.tsx +2 -1
  146. package/web/.next/standalone/packages/web/src/components/sessions/SessionDiff.tsx +3 -2
  147. package/web/.next/standalone/packages/web/src/components/sessions/SessionPreview.tsx +3 -2
  148. package/web/.next/standalone/packages/web/src/components/sessions/SessionTerminal.tsx +1 -1
  149. package/web/.next/standalone/packages/web/src/features/dashboard/DashboardClient.tsx +10 -11
  150. package/web/.next/standalone/packages/web/src/features/dashboard/components/DashboardDialogs.tsx +4 -142
  151. package/web/.next/standalone/packages/web/src/features/dashboard/components/WorkspaceOverview.tsx +20 -4
  152. package/web/.next/standalone/packages/web/src/features/sessions/SessionPageClient.tsx +10 -0
  153. package/web/.next/standalone/packages/web/src/hooks/useAgents.ts +1 -1
  154. package/web/.next/standalone/packages/web/src/hooks/useNotificationAlerts.ts +291 -0
  155. package/web/.next/standalone/packages/web/src/lib/notificationSounds.ts +207 -0
  156. package/web/.next/standalone/packages/web/src/lib/sessionState.ts +11 -6
  157. package/web/.next/static/chunks/131f2cf9c96f2724.js +1 -0
  158. package/web/.next/static/chunks/{96b41c96a34195c2.js → 437b46a75e1c92a7.js} +1 -1
  159. package/web/.next/static/chunks/450e4de887dcd142.js +1 -0
  160. package/web/.next/static/chunks/{81e3cea43881b17d.js → 4b3ac491753f4910.js} +1 -1
  161. package/web/.next/static/chunks/4f912a60e3753a1f.css +4 -0
  162. package/web/.next/static/chunks/581440483618ce47.js +1 -0
  163. package/web/.next/static/chunks/5c60a749d318d388.js +1 -0
  164. package/web/.next/static/chunks/5cb35e89898ff9e1.js +1 -0
  165. package/web/.next/static/chunks/9b1065a7ccd69a3e.js +1 -0
  166. package/web/.next/{standalone/packages/web/.next/static/chunks/355bedd41c2e9b0c.js → static/chunks/ae6e408453a5d1dc.js} +1 -1
  167. package/web/.next/static/chunks/b1c03c2c194b81b8.js +1 -0
  168. package/web/.next/static/chunks/b9c1238cc9302baa.js +1 -0
  169. package/web/.next/static/chunks/{378daaa8dbf5309c.js → c1f519b055b3c31e.js} +1 -1
  170. package/web/.next/static/chunks/c3443f28deeae499.js +1 -0
  171. package/web/.next/static/chunks/{4f9d3a3c14fb43da.js → c8e8ff2836531532.js} +1 -1
  172. package/web/.next/static/chunks/cb1a507b11d64bfb.js +1 -0
  173. package/web/.next/static/chunks/ce2f01a7e421d6fe.js +1 -0
  174. package/web/.next/static/chunks/{d934d0e74594a818.js → d27a0b78d8b5f352.js} +1 -1
  175. package/web/.next/static/chunks/{a70ab593d305d020.js → d460861f6cba32ab.js} +1 -1
  176. package/web/.next/static/chunks/dd6469b26407178b.js +1 -0
  177. package/web/.next/static/chunks/e59b8003fba5213d.js +1 -0
  178. package/web/.next/static/chunks/{459f77e3c65b47d6.js → fc68108d32aea368.js} +1 -1
  179. package/web/.next/static/chunks/{turbopack-9809b4096e86149a.js → turbopack-c76ead44073de602.js} +1 -1
  180. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__337468fd._.js +0 -3
  181. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_98876927._.js +0 -3
  182. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_883c65c7._.js +0 -3
  183. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_284bf164._.js +0 -3
  184. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_fc0422b6._.js +0 -3
  185. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_lib_cn_ts_d08d265f._.js +0 -3
  186. package/web/.next/standalone/packages/web/.next/static/chunks/26a7e7bfa0c7c333.js +0 -1
  187. package/web/.next/standalone/packages/web/.next/static/chunks/2760ffd80cc5974a.js +0 -1
  188. package/web/.next/standalone/packages/web/.next/static/chunks/27e63de286c66b73.js +0 -1
  189. package/web/.next/standalone/packages/web/.next/static/chunks/3066ce838f62bf36.js +0 -1
  190. package/web/.next/standalone/packages/web/.next/static/chunks/34500baf24f4b1ba.js +0 -1
  191. package/web/.next/standalone/packages/web/.next/static/chunks/5d8c72d5215f5485.js +0 -1
  192. package/web/.next/standalone/packages/web/.next/static/chunks/6e98107b81bc5b12.js +0 -1
  193. package/web/.next/standalone/packages/web/.next/static/chunks/8216f40bafea0ec2.js +0 -1
  194. package/web/.next/standalone/packages/web/.next/static/chunks/997ac5c24d1d89f2.js +0 -1
  195. package/web/.next/standalone/packages/web/.next/static/chunks/a0656cf00243d2a4.js +0 -1
  196. package/web/.next/standalone/packages/web/.next/static/chunks/c2f9fe243cc8bd24.js +0 -1
  197. package/web/.next/standalone/packages/web/.next/static/chunks/c3a61db21bd52eaf.css +0 -4
  198. package/web/.next/standalone/packages/web/.next/static/chunks/da9681f9820fc6bd.js +0 -1
  199. package/web/.next/static/chunks/26a7e7bfa0c7c333.js +0 -1
  200. package/web/.next/static/chunks/2760ffd80cc5974a.js +0 -1
  201. package/web/.next/static/chunks/27e63de286c66b73.js +0 -1
  202. package/web/.next/static/chunks/3066ce838f62bf36.js +0 -1
  203. package/web/.next/static/chunks/34500baf24f4b1ba.js +0 -1
  204. package/web/.next/static/chunks/5d8c72d5215f5485.js +0 -1
  205. package/web/.next/static/chunks/6e98107b81bc5b12.js +0 -1
  206. package/web/.next/static/chunks/8216f40bafea0ec2.js +0 -1
  207. package/web/.next/static/chunks/997ac5c24d1d89f2.js +0 -1
  208. package/web/.next/static/chunks/a0656cf00243d2a4.js +0 -1
  209. package/web/.next/static/chunks/c2f9fe243cc8bd24.js +0 -1
  210. package/web/.next/static/chunks/c3a61db21bd52eaf.css +0 -4
  211. package/web/.next/static/chunks/da9681f9820fc6bd.js +0 -1
  212. /package/web/.next/standalone/packages/web/.next/static/{AThcyGsbhfLjpe2EymN_C → f0rVnovCaoMI-hw2yYDek}/_buildManifest.js +0 -0
  213. /package/web/.next/standalone/packages/web/.next/static/{AThcyGsbhfLjpe2EymN_C → f0rVnovCaoMI-hw2yYDek}/_clientMiddlewareManifest.json +0 -0
  214. /package/web/.next/standalone/packages/web/.next/static/{AThcyGsbhfLjpe2EymN_C → f0rVnovCaoMI-hw2yYDek}/_ssgManifest.js +0 -0
  215. /package/web/.next/static/{AThcyGsbhfLjpe2EymN_C → f0rVnovCaoMI-hw2yYDek}/_buildManifest.js +0 -0
  216. /package/web/.next/static/{AThcyGsbhfLjpe2EymN_C → f0rVnovCaoMI-hw2yYDek}/_clientMiddlewareManifest.json +0 -0
  217. /package/web/.next/static/{AThcyGsbhfLjpe2EymN_C → f0rVnovCaoMI-hw2yYDek}/_ssgManifest.js +0 -0
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
4
3
  import { GitBranchIcon, LockIcon, MarkGithubIcon, RepoIcon } from "@primer/octicons-react";
5
4
  import {
6
5
  getAvailableAgentModels,
@@ -25,7 +24,6 @@ import {
25
24
  Building2,
26
25
  Check,
27
26
  ChevronDown,
28
- ChevronsRight,
29
27
  Copy,
30
28
  FolderGit2,
31
29
  FolderKanban,
@@ -46,6 +44,7 @@ import {
46
44
  import { normalizeAgentName } from "@/lib/agentUtils";
47
45
  import { getKnownAgent, KNOWN_AGENT_ORDER } from "@/lib/knownAgents";
48
46
  import { AgentTileIcon } from "@/components/AgentTileIcon";
47
+ import { playNotificationSound } from "@/lib/notificationSounds";
49
48
  import { normalizeModelAccessPreferences } from "@/lib/modelAccess";
50
49
  import {
51
50
  getRuntimeCatalogDefaultModelForAccess,
@@ -161,9 +160,10 @@ function formatRepoUpdatedLabel(value: string | null | undefined): string | null
161
160
  if (!value) return null;
162
161
  const timestamp = Date.parse(value);
163
162
  if (Number.isNaN(timestamp)) return null;
164
- return `Updated ${new Intl.DateTimeFormat(undefined, {
163
+ return `Updated ${new Intl.DateTimeFormat("en-US", {
165
164
  month: "short",
166
165
  day: "numeric",
166
+ timeZone: "UTC",
167
167
  }).format(new Date(timestamp))}`;
168
168
  }
169
169
 
@@ -387,18 +387,6 @@ const ONBOARDING_TABS: SettingsTab[] = [
387
387
  { id: "repositories", label: "Repository", icon: FolderGit2, implemented: true },
388
388
  ];
389
389
 
390
- const IDE_OPTIONS = [
391
- { id: "vscode", label: "VS Code" },
392
- { id: "vscode-insiders", label: "VS Code Insiders" },
393
- { id: "cursor", label: "Cursor" },
394
- { id: "windsurf", label: "Windsurf" },
395
- { id: "intellij-idea", label: "IntelliJ IDEA" },
396
- { id: "zed", label: "Zed" },
397
- { id: "xcode", label: "Xcode" },
398
- { id: "antigravity", label: "Antigravity" },
399
- { id: "custom", label: "Custom" },
400
- ];
401
-
402
390
  const MARKDOWN_EDITOR_OPTIONS = [
403
391
  { id: "obsidian", label: "Obsidian" },
404
392
  { id: "vscode", label: "VS Code" },
@@ -408,12 +396,6 @@ const MARKDOWN_EDITOR_OPTIONS = [
408
396
  { id: "custom", label: "Custom" },
409
397
  ];
410
398
 
411
- const IDE_SUBMENU_OPTIONS = IDE_OPTIONS.filter((option) => option.id !== "custom");
412
-
413
- function resolveIdeOption(editorId: string): { id: string; label: string } {
414
- return IDE_OPTIONS.find((option) => option.id === editorId) ?? { id: editorId, label: editorId };
415
- }
416
-
417
399
  const NOTIFICATION_SOUND_OPTIONS = [
418
400
  { id: "abstract-sound-1", label: "Abstract Sound 1" },
419
401
  { id: "abstract-sound-2", label: "Abstract Sound 2" },
@@ -822,35 +804,6 @@ function getAgentModelAccessLabel(agent: string, modelAccess: ModelAccessPrefere
822
804
  }
823
805
 
824
806
  const MARKDOWN_EDITOR_ICON_CLASS = "block h-4 w-4 shrink-0";
825
- const CODE_EDITOR_ICON_CLASS = "block h-4 w-4 shrink-0 object-contain";
826
-
827
- type CodeEditorIconSpec =
828
- | { kind: "icon"; icon: IconType; className: string }
829
- | { kind: "image"; imageSrc: string; className: string };
830
-
831
- const CODE_EDITOR_ICON_MAP: Record<string, CodeEditorIconSpec> = {
832
- vscode: { kind: "image", imageSrc: "/icons/ide/vscode-dark.svg", className: CODE_EDITOR_ICON_CLASS },
833
- "vscode-insiders": { kind: "image", imageSrc: "/icons/ide/vscode-insiders.svg", className: CODE_EDITOR_ICON_CLASS },
834
- cursor: { kind: "image", imageSrc: "/icons/ide/cursor-dark.svg", className: CODE_EDITOR_ICON_CLASS },
835
- windsurf: { kind: "image", imageSrc: "/icons/ide/windsurf-dark.svg", className: CODE_EDITOR_ICON_CLASS },
836
- "intellij-idea": { kind: "image", imageSrc: "/icons/ide/intellij.svg", className: CODE_EDITOR_ICON_CLASS },
837
- zed: { kind: "image", imageSrc: "/icons/ide/zed-dark.svg", className: CODE_EDITOR_ICON_CLASS },
838
- xcode: { kind: "image", imageSrc: "/icons/ide/xcode.svg", className: CODE_EDITOR_ICON_CLASS },
839
- antigravity: { kind: "image", imageSrc: "/icons/ide/antigravity-dark.svg", className: CODE_EDITOR_ICON_CLASS },
840
- custom: { kind: "icon", icon: Settings2, className: `${CODE_EDITOR_ICON_CLASS} text-[var(--vk-text-muted)]` },
841
- };
842
-
843
- function CodeEditorIcon({ editorId, label }: { editorId: string; label: string }) {
844
- const iconSpec = CODE_EDITOR_ICON_MAP[editorId];
845
- if (!iconSpec) {
846
- return <Settings2 className={`${CODE_EDITOR_ICON_CLASS} text-[var(--vk-text-muted)]`} />;
847
- }
848
- if (iconSpec.kind === "image") {
849
- return <img src={iconSpec.imageSrc} alt={`${label} logo`} className={iconSpec.className} />;
850
- }
851
- const Icon = iconSpec.icon;
852
- return <Icon className={iconSpec.className} />;
853
- }
854
807
 
855
808
  function shellQuote(value: string): string {
856
809
  return JSON.stringify(value);
@@ -2577,7 +2530,6 @@ function hydrateRepositoryDraft(value: RepositorySettingsPayload): RepositorySet
2577
2530
  const managedRemoteProvider = remoteAccessSettings.provider ?? remoteAccessSettings.recommendedProvider;
2578
2531
  const usingPrivateNetworkFlow = managedRemoteProvider === "tailscale";
2579
2532
  const showManagedTunnelControls = managedRemoteProvider !== null || remoteAccessSettings.managed;
2580
- const selectedIdeOption = resolveIdeOption(ide);
2581
2533
  const settingsMenuClass = "z-50 min-w-[240px] rounded-[6px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] p-2 shadow-[0_18px_50px_rgba(0,0,0,0.35)]";
2582
2534
  const settingsSubMenuClass = `${settingsMenuClass} min-w-[280px]`;
2583
2535
  const settingsMenuItemClass = "flex min-h-[40px] cursor-default items-center gap-2 rounded-[4px] px-3 py-2 text-[14px] leading-[21px] text-[var(--vk-text-normal)] outline-none hover:bg-[var(--vk-bg-hover)] focus:bg-[var(--vk-bg-hover)]";
@@ -2955,97 +2907,6 @@ function hydrateRepositoryDraft(value: RepositorySettingsPayload): RepositorySet
2955
2907
 
2956
2908
  {(isPreferencesTab || isGeneralTab) && (
2957
2909
  <>
2958
- <section className="space-y-3">
2959
- <div className="space-y-1">
2960
- <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Choose Your Code Editor</h4>
2961
- <p className="text-[12px] text-[var(--vk-text-muted)]">
2962
- This editor will be used when opening attempts and files.
2963
- </p>
2964
- </div>
2965
- <div className="flex flex-wrap items-center gap-2">
2966
- <DropdownMenu.Root>
2967
- <DropdownMenu.Trigger asChild>
2968
- <button
2969
- type="button"
2970
- className="inline-flex h-11 items-center gap-2 rounded-[6px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-3 text-[14px] text-[var(--vk-text-normal)] outline-none transition hover:bg-[var(--vk-bg-hover)] data-[state=open]:bg-[var(--vk-bg-hover)]"
2971
- aria-label="Choose code editor"
2972
- >
2973
- <span className="inline-flex h-7 w-7 items-center justify-center rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-main)] text-[var(--vk-text-muted)]">
2974
- <FolderOpen className="h-3.5 w-3.5" />
2975
- </span>
2976
- <span className="font-medium text-[var(--vk-text-strong)]">Open</span>
2977
- <ChevronDown className="h-3.5 w-3.5 text-[var(--vk-text-muted)]" />
2978
- </button>
2979
- </DropdownMenu.Trigger>
2980
- <DropdownMenu.Portal>
2981
- <DropdownMenu.Content align="start" sideOffset={6} className={settingsMenuClass}>
2982
- <p className="px-3 pb-1 text-[12px] font-medium uppercase tracking-[0.08em] text-[var(--vk-text-muted)]">
2983
- Open With
2984
- </p>
2985
- <DropdownMenu.Sub>
2986
- <DropdownMenu.SubTrigger className={`${settingsMenuItemClass} min-w-[220px] justify-between`}>
2987
- <div className="flex min-w-0 items-center gap-2">
2988
- <CodeEditorIcon editorId={selectedIdeOption.id} label={selectedIdeOption.label} />
2989
- <span>IDE</span>
2990
- </div>
2991
- <div className="ml-4 flex min-w-0 items-center gap-2">
2992
- <span className="truncate text-[12px] text-[var(--vk-text-muted)]">
2993
- {selectedIdeOption.label}
2994
- </span>
2995
- <ChevronsRight className="h-3.5 w-3.5 shrink-0 text-[var(--vk-text-muted)]" />
2996
- </div>
2997
- </DropdownMenu.SubTrigger>
2998
- <DropdownMenu.Portal>
2999
- <DropdownMenu.SubContent
3000
- sideOffset={8}
3001
- alignOffset={-4}
3002
- className={settingsSubMenuClass}
3003
- >
3004
- <p className="px-3 pb-1 text-[12px] font-medium uppercase tracking-[0.08em] text-[var(--vk-text-muted)]">
3005
- Editors
3006
- </p>
3007
- {IDE_SUBMENU_OPTIONS.map((option) => (
3008
- <DropdownMenu.Item
3009
- key={option.id}
3010
- onSelect={() => setIde(option.id)}
3011
- className={settingsMenuItemClass}
3012
- >
3013
- <CodeEditorIcon editorId={option.id} label={option.label} />
3014
- <span className="flex-1">{option.label}</span>
3015
- <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
3016
- {ide === option.id ? <Check className="h-4 w-4 text-[var(--vk-orange)]" /> : null}
3017
- </span>
3018
- </DropdownMenu.Item>
3019
- ))}
3020
- </DropdownMenu.SubContent>
3021
- </DropdownMenu.Portal>
3022
- </DropdownMenu.Sub>
3023
- <DropdownMenu.Separator className="my-1 h-px bg-[var(--vk-border)]" />
3024
- <DropdownMenu.Item onSelect={() => setIde("custom")} className={settingsMenuItemClass}>
3025
- <CodeEditorIcon editorId="custom" label="Custom" />
3026
- <span className="flex-1">Custom</span>
3027
- <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
3028
- {ide === "custom" ? <Check className="h-4 w-4 text-[var(--vk-orange)]" /> : null}
3029
- </span>
3030
- </DropdownMenu.Item>
3031
- </DropdownMenu.Content>
3032
- </DropdownMenu.Portal>
3033
- </DropdownMenu.Root>
3034
-
3035
- <div className="inline-flex min-h-11 max-w-full items-center gap-2 rounded-[6px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-3 py-2">
3036
- <span className="inline-flex h-7 w-7 items-center justify-center rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-main)]">
3037
- <CodeEditorIcon editorId={selectedIdeOption.id} label={selectedIdeOption.label} />
3038
- </span>
3039
- <div className="min-w-0">
3040
- <div className="truncate text-[14px] font-medium text-[var(--vk-text-strong)]">
3041
- {selectedIdeOption.label}
3042
- </div>
3043
- <div className="truncate text-[11px] text-[var(--vk-text-muted)]">Current editor</div>
3044
- </div>
3045
- </div>
3046
- </div>
3047
- </section>
3048
-
3049
2910
  <section className="space-y-2">
3050
2911
  <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Markdown Editor</h4>
3051
2912
  <p className="text-[12px] text-[var(--vk-text-muted)]">
@@ -3127,6 +2988,7 @@ function hydrateRepositoryDraft(value: RepositorySettingsPayload): RepositorySet
3127
2988
  onClick={() => {
3128
2989
  setSoundEnabled(true);
3129
2990
  setSoundFile(option.id);
2991
+ void playNotificationSound(option.id);
3130
2992
  }}
3131
2993
  className={`flex items-center gap-2 rounded-[4px] border px-3 py-2 text-left ${
3132
2994
  selected
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo } from "react";
3
+ import { useEffect, useMemo, useState } from "react";
4
4
  import {
5
5
  ArrowRight,
6
6
  FolderGit2,
@@ -22,8 +22,11 @@ interface WorkspaceOverviewProps {
22
22
  onSelectSession: (sessionId: string) => void;
23
23
  }
24
24
 
25
- function formatRelativeTime(isoDate: string): string {
26
- const diffMs = Date.now() - new Date(isoDate).getTime();
25
+ const RELATIVE_TIME_TICK_MS = 60_000;
26
+
27
+ function formatRelativeTime(isoDate: string, now: number | null): string {
28
+ if (now === null) return "Updated recently";
29
+ const diffMs = now - new Date(isoDate).getTime();
27
30
  if (!Number.isFinite(diffMs) || diffMs < 60_000) return "Updated now";
28
31
  const minutes = Math.floor(diffMs / 60_000);
29
32
  if (minutes < 60) return `Updated ${minutes}m ago`;
@@ -92,6 +95,17 @@ export function WorkspaceOverview({
92
95
  onCreateWorkspace,
93
96
  onSelectSession,
94
97
  }: WorkspaceOverviewProps) {
98
+ const [relativeNow, setRelativeNow] = useState<number | null>(null);
99
+
100
+ useEffect(() => {
101
+ setRelativeNow(Date.now());
102
+ const interval = window.setInterval(() => {
103
+ setRelativeNow(Date.now());
104
+ }, RELATIVE_TIME_TICK_MS);
105
+
106
+ return () => window.clearInterval(interval);
107
+ }, []);
108
+
95
109
  const visibleSessions = useMemo(
96
110
  () => sessions.filter((session) => session.status !== "archived"),
97
111
  [sessions],
@@ -252,7 +266,9 @@ export function WorkspaceOverview({
252
266
  </p>
253
267
  </div>
254
268
  <div className="shrink-0 text-right">
255
- <p className="text-[11px] text-[var(--vk-text-muted)]">{formatRelativeTime(session.lastActivityAt)}</p>
269
+ <p className="text-[11px] text-[var(--vk-text-muted)]">
270
+ {formatRelativeTime(session.lastActivityAt, relativeNow)}
271
+ </p>
256
272
  <ArrowRight className="ml-auto mt-2 h-4 w-4 text-[var(--vk-text-muted)]" />
257
273
  </div>
258
274
  </button>
@@ -9,6 +9,8 @@ import { WorkspaceSidebarPanel } from "@/components/layout/WorkspaceSidebarPanel
9
9
  import { SessionDetail } from "@/components/sessions/SessionDetail";
10
10
  import { shouldUseCompactTerminalChrome } from "@/components/sessions/sessionTerminalUtils";
11
11
  import { useConfig } from "@/hooks/useConfig";
12
+ import { useNotificationAlerts } from "@/hooks/useNotificationAlerts";
13
+ import { usePreferences } from "@/hooks/usePreferences";
12
14
  import { useSession } from "@/hooks/useSession";
13
15
  import { useSessions } from "@/hooks/useSessions";
14
16
  import type { DashboardSession } from "@/lib/types";
@@ -19,6 +21,7 @@ export default function SessionPageClient() {
19
21
  const searchParams = useSearchParams();
20
22
  const { projects } = useConfig();
21
23
  const { session: currentSession } = useSession(params.id);
24
+ const { preferences, loading: preferencesLoading } = usePreferences();
22
25
  const {
23
26
  mobileSidebarOpen,
24
27
  desktopSidebarOpen,
@@ -35,6 +38,13 @@ export default function SessionPageClient() {
35
38
  return tab !== "overview" && tab !== "preview" && tab !== "diff";
36
39
  }, [searchParams]);
37
40
  const immersiveTerminalMode = terminalTabActive && compactTerminalChrome;
41
+ const notificationProjectId = currentSession?.projectId ?? null;
42
+
43
+ useNotificationAlerts({
44
+ enabled: !preferencesLoading && notificationProjectId !== null,
45
+ projectId: notificationProjectId,
46
+ preferences: preferences?.notifications ?? null,
47
+ });
38
48
 
39
49
  const topBarTitle = useMemo(() => {
40
50
  if (currentSession) {
@@ -126,7 +126,7 @@ async function refreshAgents(force = false): Promise<void> {
126
126
  }
127
127
 
128
128
  export function useAgents(): UseAgentsReturn {
129
- const [snapshot, setSnapshot] = useState<AgentsStoreSnapshot>(() => currentSnapshot());
129
+ const [snapshot, setSnapshot] = useState<AgentsStoreSnapshot>({ agents: [], loading: true });
130
130
 
131
131
  useEffect(() => {
132
132
  const applySnapshot = () => setSnapshot(currentSnapshot());
@@ -0,0 +1,291 @@
1
+ "use client";
2
+
3
+ import { type MutableRefObject, useEffect, useRef } from "react";
4
+ import {
5
+ primeNotificationAudio,
6
+ playNotificationSound,
7
+ resolveNotificationSoundId,
8
+ type NotificationSoundId,
9
+ } from "@/lib/notificationSounds";
10
+
11
+ type NotificationPriority = "high" | "medium" | "low";
12
+
13
+ type NotificationRecord = {
14
+ id: string;
15
+ priority: NotificationPriority;
16
+ message: string;
17
+ timestamp: string;
18
+ sessionId: string;
19
+ projectId: string;
20
+ type: string;
21
+ };
22
+
23
+ type NotificationResponse = {
24
+ notifications?: unknown;
25
+ };
26
+
27
+ interface NotificationPreferences {
28
+ soundEnabled: boolean;
29
+ soundFile: string | null;
30
+ }
31
+
32
+ interface UseNotificationAlertsOptions {
33
+ enabled: boolean;
34
+ projectId: string | null;
35
+ preferences: NotificationPreferences | null;
36
+ }
37
+
38
+ const POLL_INTERVAL_MS = 10_000;
39
+
40
+ function isVisiblePage(): boolean {
41
+ return typeof document === "undefined" || document.visibilityState === "visible";
42
+ }
43
+
44
+ function isNotificationPriority(value: unknown): value is NotificationPriority {
45
+ return value === "high" || value === "medium" || value === "low";
46
+ }
47
+
48
+ function toObject(value: unknown): Record<string, unknown> {
49
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
50
+ return { ...(value as Record<string, unknown>) };
51
+ }
52
+
53
+ function normalizeNotification(value: unknown): NotificationRecord | null {
54
+ const payload = toObject(value);
55
+ const id = typeof payload.id === "string" ? payload.id.trim() : "";
56
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
57
+ const timestamp = typeof payload.timestamp === "string" ? payload.timestamp.trim() : "";
58
+ const sessionId = typeof payload.sessionId === "string" ? payload.sessionId.trim() : "";
59
+ const projectId = typeof payload.projectId === "string" ? payload.projectId.trim() : "";
60
+ const type = typeof payload.type === "string" ? payload.type.trim() : "";
61
+ const priority = isNotificationPriority(payload.priority) ? payload.priority : "low";
62
+
63
+ if (!id || !message || !timestamp || !sessionId || !projectId || !type) {
64
+ return null;
65
+ }
66
+
67
+ return {
68
+ id,
69
+ priority,
70
+ message,
71
+ timestamp,
72
+ sessionId,
73
+ projectId,
74
+ type,
75
+ };
76
+ }
77
+
78
+ function normalizeNotificationResponse(value: unknown): NotificationRecord[] {
79
+ const payload = toObject(value);
80
+ const notifications = Array.isArray(payload.notifications)
81
+ ? payload.notifications
82
+ : [];
83
+ return notifications
84
+ .map((notification) => normalizeNotification(notification))
85
+ .filter((notification): notification is NotificationRecord => notification !== null)
86
+ .sort((left, right) => {
87
+ const timestampOrder = Date.parse(right.timestamp) - Date.parse(left.timestamp);
88
+ if (timestampOrder !== 0) return timestampOrder;
89
+ return right.id.localeCompare(left.id);
90
+ });
91
+ }
92
+
93
+ function latestTimestamp(notifications: NotificationRecord[]): string | null {
94
+ return notifications[0]?.timestamp ?? null;
95
+ }
96
+
97
+ function shouldAlert(notification: NotificationRecord): boolean {
98
+ return notification.priority !== "low";
99
+ }
100
+
101
+ function pickAlertNotification(notifications: NotificationRecord[]): NotificationRecord | null {
102
+ return notifications.find((notification) => notification.priority === "high")
103
+ ?? notifications.find((notification) => notification.priority === "medium")
104
+ ?? notifications[0]
105
+ ?? null;
106
+ }
107
+
108
+ function notificationKey(notification: NotificationRecord): string {
109
+ return `${notification.id}::${notification.timestamp}`;
110
+ }
111
+
112
+ function clearTimer(timerRef: MutableRefObject<number | null>) {
113
+ if (timerRef.current !== null) {
114
+ window.clearTimeout(timerRef.current);
115
+ timerRef.current = null;
116
+ }
117
+ }
118
+
119
+ export function useNotificationAlerts({
120
+ enabled,
121
+ projectId,
122
+ preferences,
123
+ }: UseNotificationAlertsOptions): void {
124
+ const soundEnabledRef = useRef(preferences?.soundEnabled !== false);
125
+ const soundFileRef = useRef<NotificationSoundId>(resolveNotificationSoundId(preferences?.soundFile));
126
+ const seenNotificationKeysRef = useRef(new Set<string>());
127
+ const latestTimestampRef = useRef<string | null>(null);
128
+ const initializedRef = useRef(false);
129
+ const activeProjectIdRef = useRef<string | null>(null);
130
+ const inFlightRef = useRef<Promise<void> | null>(null);
131
+ const pollTimerRef = useRef<number | null>(null);
132
+
133
+ useEffect(() => {
134
+ soundEnabledRef.current = preferences?.soundEnabled !== false;
135
+ soundFileRef.current = resolveNotificationSoundId(preferences?.soundFile);
136
+ }, [preferences?.soundEnabled, preferences?.soundFile]);
137
+
138
+ useEffect(() => {
139
+ if (!enabled || typeof window === "undefined") {
140
+ return;
141
+ }
142
+
143
+ const normalizedProjectId = projectId?.trim() || null;
144
+ if (activeProjectIdRef.current !== normalizedProjectId) {
145
+ activeProjectIdRef.current = normalizedProjectId;
146
+ seenNotificationKeysRef.current = new Set();
147
+ latestTimestampRef.current = null;
148
+ initializedRef.current = false;
149
+ }
150
+
151
+ let cancelled = false;
152
+
153
+ const loadNotifications = async (initial: boolean): Promise<void> => {
154
+ if (cancelled || !isVisiblePage()) {
155
+ return;
156
+ }
157
+
158
+ if (inFlightRef.current) {
159
+ await inFlightRef.current;
160
+ return;
161
+ }
162
+
163
+ const load = (async () => {
164
+ const params = new URLSearchParams({
165
+ limit: "20",
166
+ });
167
+ if (normalizedProjectId) {
168
+ params.set("project", normalizedProjectId);
169
+ }
170
+ if (!initial && latestTimestampRef.current) {
171
+ params.set("since", latestTimestampRef.current);
172
+ }
173
+
174
+ try {
175
+ const response = await fetch(`/api/notifications?${params.toString()}`, {
176
+ cache: "no-store",
177
+ });
178
+ if (!response.ok) {
179
+ return;
180
+ }
181
+
182
+ const payload = (await response.json().catch(() => null)) as NotificationResponse | null;
183
+ const notifications = normalizeNotificationResponse(payload);
184
+ if (notifications.length === 0) {
185
+ initializedRef.current = true;
186
+ return;
187
+ }
188
+
189
+ latestTimestampRef.current = latestTimestamp(notifications);
190
+
191
+ if (initial || !initializedRef.current) {
192
+ for (const notification of notifications) {
193
+ seenNotificationKeysRef.current.add(notificationKey(notification));
194
+ }
195
+ initializedRef.current = true;
196
+ return;
197
+ }
198
+
199
+ const unseenNotifications = notifications.filter(
200
+ (notification) => !seenNotificationKeysRef.current.has(notificationKey(notification)),
201
+ );
202
+
203
+ for (const notification of notifications) {
204
+ seenNotificationKeysRef.current.add(notificationKey(notification));
205
+ }
206
+
207
+ if (unseenNotifications.length === 0) {
208
+ return;
209
+ }
210
+
211
+ const alertNotification = pickAlertNotification(unseenNotifications);
212
+ if (!alertNotification || !shouldAlert(alertNotification) || !soundEnabledRef.current) {
213
+ return;
214
+ }
215
+
216
+ await playNotificationSound(soundFileRef.current);
217
+ } catch {
218
+ // Ignore transient notification polling failures.
219
+ }
220
+ })();
221
+
222
+ inFlightRef.current = load;
223
+ try {
224
+ await load;
225
+ } finally {
226
+ if (inFlightRef.current === load) {
227
+ inFlightRef.current = null;
228
+ }
229
+ initializedRef.current = true;
230
+ }
231
+ };
232
+
233
+ const scheduleNextPoll = () => {
234
+ clearTimer(pollTimerRef);
235
+ if (cancelled || !isVisiblePage()) {
236
+ return;
237
+ }
238
+
239
+ pollTimerRef.current = window.setTimeout(() => {
240
+ void loadNotifications(false).finally(() => {
241
+ scheduleNextPoll();
242
+ });
243
+ }, POLL_INTERVAL_MS);
244
+ };
245
+
246
+ const startPolling = () => {
247
+ if (cancelled || !isVisiblePage()) {
248
+ return;
249
+ }
250
+
251
+ void loadNotifications(!initializedRef.current).finally(() => {
252
+ scheduleNextPoll();
253
+ });
254
+ };
255
+
256
+ const handleVisibilityChange = () => {
257
+ if (!isVisiblePage()) {
258
+ clearTimer(pollTimerRef);
259
+ return;
260
+ }
261
+ startPolling();
262
+ };
263
+
264
+ const handleFocus = () => {
265
+ if (isVisiblePage()) {
266
+ startPolling();
267
+ }
268
+ };
269
+
270
+ const handleAudioPrime = () => {
271
+ void primeNotificationAudio();
272
+ };
273
+
274
+ startPolling();
275
+ window.addEventListener("focus", handleFocus);
276
+ document.addEventListener("visibilitychange", handleVisibilityChange);
277
+ window.addEventListener("pointerdown", handleAudioPrime, true);
278
+ window.addEventListener("touchstart", handleAudioPrime, true);
279
+ window.addEventListener("keydown", handleAudioPrime, true);
280
+
281
+ return () => {
282
+ cancelled = true;
283
+ clearTimer(pollTimerRef);
284
+ window.removeEventListener("focus", handleFocus);
285
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
286
+ window.removeEventListener("pointerdown", handleAudioPrime, true);
287
+ window.removeEventListener("touchstart", handleAudioPrime, true);
288
+ window.removeEventListener("keydown", handleAudioPrime, true);
289
+ };
290
+ }, [enabled, projectId]);
291
+ }