conductor-oss 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) 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/_2c837d66._.js +1 -1
  90. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__cb0aeb63._.js → [root-of-the-server]__1329224f._.js} +2 -2
  91. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__ce4b4158._.js → [root-of-the-server]__1cb74434._.js} +1 -1
  92. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__53bcdbc9._.js +1 -1
  93. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__c405048c._.js +1 -1
  94. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__c79fcfd3._.js +3 -0
  95. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
  96. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_3aee3cff._.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/_9990bc82._.js +3 -0
  102. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_a990354c._.js +1 -1
  103. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b9330c69._.js +1 -1
  104. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
  105. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
  106. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_152e81cd._.js +3 -0
  107. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_a09380f4._.js +3 -0
  108. 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_e767206a._.js} +2 -2
  109. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@radix-ui_19d0f177._.js +3 -0
  110. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_b8d99fb4._.js → node_modules_@radix-ui_ef419f6b._.js} +2 -2
  111. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_core_dist_types_ba19386c.js +1 -1
  112. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_components_board_WorkspaceKanban_tsx_735b7999._.js +1 -1
  113. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_components_sessions_SessionPreview_tsx_ec32db81._.js +1 -1
  114. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_features_dashboard_DashboardClient_tsx_81ae42b0._.js +1 -1
  115. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_features_dashboard_components_DashboardDialogs_tsx_32d3d858._.js +1 -1
  116. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_hooks_5cb69d9a._.js +1 -1
  117. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_hooks_f05b02cd._.js +1 -1
  118. package/web/.next/standalone/packages/web/.next/server/middleware-build-manifest.js +2 -2
  119. package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
  120. package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
  121. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  122. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
  123. package/web/.next/standalone/packages/web/.next/static/chunks/{d934d0e74594a818.js → 2d65cd3bc1137b3d.js} +1 -1
  124. package/web/.next/standalone/packages/web/.next/static/chunks/{96b41c96a34195c2.js → 437b46a75e1c92a7.js} +1 -1
  125. package/web/.next/standalone/packages/web/.next/static/chunks/450e4de887dcd142.js +1 -0
  126. package/web/.next/standalone/packages/web/.next/static/chunks/{81e3cea43881b17d.js → 4b3ac491753f4910.js} +1 -1
  127. package/web/.next/standalone/packages/web/.next/static/chunks/4f912a60e3753a1f.css +4 -0
  128. package/web/.next/standalone/packages/web/.next/static/chunks/50475743a65c6ae5.js +1 -0
  129. package/web/.next/standalone/packages/web/.next/static/chunks/581440483618ce47.js +1 -0
  130. package/web/.next/standalone/packages/web/.next/static/chunks/5c60a749d318d388.js +1 -0
  131. package/web/.next/standalone/packages/web/.next/static/chunks/5cb35e89898ff9e1.js +1 -0
  132. package/web/.next/standalone/packages/web/.next/static/chunks/{378daaa8dbf5309c.js → 741a641e5fe294ff.js} +1 -1
  133. package/web/.next/standalone/packages/web/.next/static/chunks/9b1065a7ccd69a3e.js +1 -0
  134. package/web/.next/standalone/packages/web/.next/static/chunks/{a70ab593d305d020.js → a0af599485430d5f.js} +1 -1
  135. package/web/.next/{static/chunks/355bedd41c2e9b0c.js → standalone/packages/web/.next/static/chunks/ae6e408453a5d1dc.js} +1 -1
  136. package/web/.next/standalone/packages/web/.next/static/chunks/b1c03c2c194b81b8.js +1 -0
  137. package/web/.next/standalone/packages/web/.next/static/chunks/b9c1238cc9302baa.js +1 -0
  138. package/web/.next/standalone/packages/web/.next/static/chunks/c3443f28deeae499.js +1 -0
  139. package/web/.next/standalone/packages/web/.next/static/chunks/{4f9d3a3c14fb43da.js → c8e8ff2836531532.js} +1 -1
  140. package/web/.next/standalone/packages/web/.next/static/chunks/cb1a507b11d64bfb.js +1 -0
  141. package/web/.next/standalone/packages/web/.next/static/chunks/ce2f01a7e421d6fe.js +1 -0
  142. package/web/.next/standalone/packages/web/.next/static/chunks/dd6469b26407178b.js +1 -0
  143. package/web/.next/standalone/packages/web/.next/static/chunks/e59b8003fba5213d.js +1 -0
  144. package/web/.next/standalone/packages/web/.next/static/chunks/{459f77e3c65b47d6.js → fc68108d32aea368.js} +1 -1
  145. package/web/.next/standalone/packages/web/.next/static/chunks/{turbopack-9809b4096e86149a.js → turbopack-c76ead44073de602.js} +1 -1
  146. package/web/.next/standalone/packages/web/src/components/board/WorkspaceKanban.tsx +2 -1
  147. package/web/.next/standalone/packages/web/src/components/sessions/SessionDiff.tsx +3 -2
  148. package/web/.next/standalone/packages/web/src/components/sessions/SessionPreview.tsx +3 -2
  149. package/web/.next/standalone/packages/web/src/components/sessions/SessionTerminal.tsx +1 -1
  150. package/web/.next/standalone/packages/web/src/features/dashboard/DashboardClient.tsx +10 -11
  151. package/web/.next/standalone/packages/web/src/features/dashboard/components/DashboardDialogs.tsx +4 -142
  152. package/web/.next/standalone/packages/web/src/features/dashboard/components/WorkspaceOverview.tsx +20 -4
  153. package/web/.next/standalone/packages/web/src/features/sessions/SessionPageClient.tsx +10 -0
  154. package/web/.next/standalone/packages/web/src/hooks/useAgents.ts +1 -1
  155. package/web/.next/standalone/packages/web/src/hooks/useNotificationAlerts.ts +291 -0
  156. package/web/.next/standalone/packages/web/src/lib/notificationSounds.ts +207 -0
  157. package/web/.next/standalone/packages/web/src/lib/sessionState.ts +11 -6
  158. package/web/.next/static/chunks/{d934d0e74594a818.js → 2d65cd3bc1137b3d.js} +1 -1
  159. package/web/.next/static/chunks/{96b41c96a34195c2.js → 437b46a75e1c92a7.js} +1 -1
  160. package/web/.next/static/chunks/450e4de887dcd142.js +1 -0
  161. package/web/.next/static/chunks/{81e3cea43881b17d.js → 4b3ac491753f4910.js} +1 -1
  162. package/web/.next/static/chunks/4f912a60e3753a1f.css +4 -0
  163. package/web/.next/static/chunks/50475743a65c6ae5.js +1 -0
  164. package/web/.next/static/chunks/581440483618ce47.js +1 -0
  165. package/web/.next/static/chunks/5c60a749d318d388.js +1 -0
  166. package/web/.next/static/chunks/5cb35e89898ff9e1.js +1 -0
  167. package/web/.next/static/chunks/{378daaa8dbf5309c.js → 741a641e5fe294ff.js} +1 -1
  168. package/web/.next/static/chunks/9b1065a7ccd69a3e.js +1 -0
  169. package/web/.next/static/chunks/{a70ab593d305d020.js → a0af599485430d5f.js} +1 -1
  170. package/web/.next/{standalone/packages/web/.next/static/chunks/355bedd41c2e9b0c.js → static/chunks/ae6e408453a5d1dc.js} +1 -1
  171. package/web/.next/static/chunks/b1c03c2c194b81b8.js +1 -0
  172. package/web/.next/static/chunks/b9c1238cc9302baa.js +1 -0
  173. package/web/.next/static/chunks/c3443f28deeae499.js +1 -0
  174. package/web/.next/static/chunks/{4f9d3a3c14fb43da.js → c8e8ff2836531532.js} +1 -1
  175. package/web/.next/static/chunks/cb1a507b11d64bfb.js +1 -0
  176. package/web/.next/static/chunks/ce2f01a7e421d6fe.js +1 -0
  177. package/web/.next/static/chunks/dd6469b26407178b.js +1 -0
  178. package/web/.next/static/chunks/e59b8003fba5213d.js +1 -0
  179. package/web/.next/static/chunks/{459f77e3c65b47d6.js → fc68108d32aea368.js} +1 -1
  180. package/web/.next/static/chunks/{turbopack-9809b4096e86149a.js → turbopack-c76ead44073de602.js} +1 -1
  181. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__337468fd._.js +0 -3
  182. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_98876927._.js +0 -3
  183. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_883c65c7._.js +0 -3
  184. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_284bf164._.js +0 -3
  185. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_fc0422b6._.js +0 -3
  186. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_lib_cn_ts_d08d265f._.js +0 -3
  187. package/web/.next/standalone/packages/web/.next/static/chunks/26a7e7bfa0c7c333.js +0 -1
  188. package/web/.next/standalone/packages/web/.next/static/chunks/2760ffd80cc5974a.js +0 -1
  189. package/web/.next/standalone/packages/web/.next/static/chunks/27e63de286c66b73.js +0 -1
  190. package/web/.next/standalone/packages/web/.next/static/chunks/3066ce838f62bf36.js +0 -1
  191. package/web/.next/standalone/packages/web/.next/static/chunks/34500baf24f4b1ba.js +0 -1
  192. package/web/.next/standalone/packages/web/.next/static/chunks/5d8c72d5215f5485.js +0 -1
  193. package/web/.next/standalone/packages/web/.next/static/chunks/6e98107b81bc5b12.js +0 -1
  194. package/web/.next/standalone/packages/web/.next/static/chunks/8216f40bafea0ec2.js +0 -1
  195. package/web/.next/standalone/packages/web/.next/static/chunks/997ac5c24d1d89f2.js +0 -1
  196. package/web/.next/standalone/packages/web/.next/static/chunks/a0656cf00243d2a4.js +0 -1
  197. package/web/.next/standalone/packages/web/.next/static/chunks/c2f9fe243cc8bd24.js +0 -1
  198. package/web/.next/standalone/packages/web/.next/static/chunks/c3a61db21bd52eaf.css +0 -4
  199. package/web/.next/standalone/packages/web/.next/static/chunks/da9681f9820fc6bd.js +0 -1
  200. package/web/.next/static/chunks/26a7e7bfa0c7c333.js +0 -1
  201. package/web/.next/static/chunks/2760ffd80cc5974a.js +0 -1
  202. package/web/.next/static/chunks/27e63de286c66b73.js +0 -1
  203. package/web/.next/static/chunks/3066ce838f62bf36.js +0 -1
  204. package/web/.next/static/chunks/34500baf24f4b1ba.js +0 -1
  205. package/web/.next/static/chunks/5d8c72d5215f5485.js +0 -1
  206. package/web/.next/static/chunks/6e98107b81bc5b12.js +0 -1
  207. package/web/.next/static/chunks/8216f40bafea0ec2.js +0 -1
  208. package/web/.next/static/chunks/997ac5c24d1d89f2.js +0 -1
  209. package/web/.next/static/chunks/a0656cf00243d2a4.js +0 -1
  210. package/web/.next/static/chunks/c2f9fe243cc8bd24.js +0 -1
  211. package/web/.next/static/chunks/c3a61db21bd52eaf.css +0 -4
  212. package/web/.next/static/chunks/da9681f9820fc6bd.js +0 -1
  213. /package/web/.next/standalone/packages/web/.next/static/{AThcyGsbhfLjpe2EymN_C → E909GLj6potSWpP-i6F73}/_buildManifest.js +0 -0
  214. /package/web/.next/standalone/packages/web/.next/static/{AThcyGsbhfLjpe2EymN_C → E909GLj6potSWpP-i6F73}/_clientMiddlewareManifest.json +0 -0
  215. /package/web/.next/standalone/packages/web/.next/static/{AThcyGsbhfLjpe2EymN_C → E909GLj6potSWpP-i6F73}/_ssgManifest.js +0 -0
  216. /package/web/.next/static/{AThcyGsbhfLjpe2EymN_C → E909GLj6potSWpP-i6F73}/_buildManifest.js +0 -0
  217. /package/web/.next/static/{AThcyGsbhfLjpe2EymN_C → E909GLj6potSWpP-i6F73}/_clientMiddlewareManifest.json +0 -0
  218. /package/web/.next/static/{AThcyGsbhfLjpe2EymN_C → E909GLj6potSWpP-i6F73}/_ssgManifest.js +0 -0
@@ -100,7 +100,7 @@ function SessionTerminalView(props: SessionTerminalProps) {
100
100
  setPromptSending(false);
101
101
  setPromptError(null);
102
102
  setQueuedInsertError(null);
103
- }, [sessionId]);
103
+ }, [expectsLiveTerminal, sessionId]);
104
104
 
105
105
  useEffect(() => {
106
106
  if (!expectsLiveTerminal) {
@@ -56,6 +56,7 @@ import {
56
56
  import { useSession } from "@/hooks/useSession";
57
57
  import { useSessions } from "@/hooks/useSessions";
58
58
  import { useConfig, type ConfigProject } from "@/hooks/useConfig";
59
+ import { useNotificationAlerts } from "@/hooks/useNotificationAlerts";
59
60
  import { useAgents } from "@/hooks/useAgents";
60
61
  import { useResponsiveSidebarStateWithOptions } from "@/hooks/useResponsiveSidebarState";
61
62
  import { AppShell } from "@/components/layout/AppShell";
@@ -242,9 +243,10 @@ function formatRepoUpdatedLabel(value: string | null | undefined): string | null
242
243
  if (!value) return null;
243
244
  const timestamp = Date.parse(value);
244
245
  if (Number.isNaN(timestamp)) return null;
245
- return `Updated ${new Intl.DateTimeFormat(undefined, {
246
+ return `Updated ${new Intl.DateTimeFormat("en-US", {
246
247
  month: "short",
247
248
  day: "numeric",
249
+ timeZone: "UTC",
248
250
  }).format(new Date(timestamp))}`;
249
251
  }
250
252
 
@@ -495,16 +497,6 @@ function resolveIdeOption(editorId: string): { id: string; label: string } {
495
497
  return IDE_OPTIONS.find((option) => option.id === editorId) ?? { id: editorId, label: editorId };
496
498
  }
497
499
 
498
- const NOTIFICATION_SOUND_OPTIONS = [
499
- { id: "abstract-sound-1", label: "Abstract Sound 1" },
500
- { id: "abstract-sound-2", label: "Abstract Sound 2" },
501
- { id: "abstract-sound-3", label: "Abstract Sound 3" },
502
- { id: "abstract-sound-4", label: "Abstract Sound 4" },
503
- { id: "cow-mooing", label: "Cow Mooing" },
504
- { id: "phone-vibration", label: "Phone Vibration" },
505
- { id: "rooster", label: "Rooster" },
506
- ];
507
-
508
500
  function toObject(value: unknown): Record<string, unknown> {
509
501
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
510
502
  return { ...(value as Record<string, unknown>) };
@@ -1791,6 +1783,13 @@ export default function DashboardClient() {
1791
1783
  const onboardingRequired = !preferencesLoading && !!preferences && !preferences.onboardingAcknowledged;
1792
1784
  const resolvedPreferences = preferences ?? normalizePreferences(null, selectedAgent || DEFAULT_AGENT);
1793
1785
  const resolvedCodingAgent = selectedAgent || resolvedPreferences.codingAgent || DEFAULT_AGENT;
1786
+ const notificationProjectId = selectedProjectId ?? selectedSessionRecord?.projectId ?? null;
1787
+
1788
+ useNotificationAlerts({
1789
+ enabled: !preferencesLoading,
1790
+ projectId: notificationProjectId,
1791
+ preferences: resolvedPreferences.notifications,
1792
+ });
1794
1793
 
1795
1794
  const handleSelectProject = useCallback((projectId: string | null) => {
1796
1795
  navigateDashboard(
@@ -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
+ }