failproofai 0.0.14-beta.2 → 0.0.15-beta.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 (269) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +2 -2
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/invite/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/status/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  47. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  49. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/index.html +1 -1
  52. package/.next/standalone/.next/server/app/index.rsc +15 -15
  53. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  54. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  55. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  56. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  57. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  59. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/page.js +2 -2
  61. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  64. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  65. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  66. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  69. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  70. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -4
  71. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  74. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  75. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  76. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  77. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  80. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  81. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  82. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0hv08mx._.js → [root-of-the-server]__0-dh5jy._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0obaznk._.js → [root-of-the-server]__0h3lfmp._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xvsnvt._.js → [root-of-the-server]__0l4vuqm._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +4 -4
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +6 -4
  94. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1hkdkmz._.js → [root-of-the-server]__10oa-v9._.js} +1 -1
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1i9rknc._.js → [root-of-the-server]__18d-9oi._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1is3glr._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +6 -6
  102. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0hdsupo._.js → [root-of-the-server]__0114ewg._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01od0m6._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0s89hoe._.js → [root-of-the-server]__02r5bgf._.js} +3 -3
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09v9cwb._.js → [root-of-the-server]__04o-kw7._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0qeuy9c._.js → [root-of-the-server]__0e8sjqm._.js} +3 -3
  110. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tkjqka._.js → [root-of-the-server]__0lkzqax._.js} +3 -3
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lpvwqm._.js +21 -0
  112. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ynf7tx._.js → [root-of-the-server]__0qucxyj._.js} +3 -3
  113. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__03lv-pe._.js → [root-of-the-server]__0u02miy._.js} +3 -3
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +8 -8
  116. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00cz9as._.js → [root-of-the-server]__1apb7k_._.js} +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +4 -0
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1i21qhv._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1py795m._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__015_i4t._.js → [root-of-the-server]__1zqz4v8._.js} +3 -3
  123. package/.next/standalone/.next/server/chunks/ssr/_077y5np._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/app_audit_loading_tsx_1j1kc6j._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_06xsmte.js → node_modules_html-to-image_es_index_0w82veb.js} +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  137. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  138. package/.next/standalone/.next/server/pages/404.html +1 -1
  139. package/.next/standalone/.next/server/pages/500.html +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  142. package/.next/standalone/.next/static/chunks/0hn30_j22wpp_.js +6 -0
  143. package/.next/standalone/.next/static/chunks/{2llkdafnaejw_.js → 1_20f96f1253b.js} +1 -1
  144. package/.next/standalone/.next/static/chunks/1hdhteui932bq.js +1 -0
  145. package/.next/standalone/.next/static/chunks/{33u59vf_8xpd-.js → 1j2xm011_aoae.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/{388hgfqye67xa.js → 1qzsbft-jtnf8.js} +1 -1
  147. package/.next/standalone/.next/static/chunks/{3-ogr79xqbw0h.js → 2mazwb1qmur1p.js} +1 -1
  148. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +2 -0
  149. package/.next/standalone/.next/static/chunks/2xbkqlma_-61k.js +1 -0
  150. package/.next/standalone/.next/static/chunks/{29zkom4s9de2d.js → 2y7acsw0--8xf.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/3060a-0fayz96.js +1 -0
  152. package/.next/standalone/.next/static/chunks/312ihvhxeuoi_.js +1 -0
  153. package/.next/standalone/.next/static/chunks/31c084gsop1bb.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{1ua5armwfph8o.js → 31fmsp1soxi07.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{02jcotghcxrbu.js → 3kmwfieo15mr2.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{1gg-8e9q_m__9.js → 3q9y67-fobgpc.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{turbopack-10hj8t5pf33tu.js → turbopack-1lsv92x7klwrn.js} +1 -1
  158. package/.next/standalone/app/actions/get-hooks-config.ts +85 -0
  159. package/.next/standalone/app/components/log-viewer/entry-row.tsx +8 -3
  160. package/.next/standalone/app/components/project-list.tsx +229 -42
  161. package/.next/standalone/app/components/raw-log-viewer.tsx +33 -15
  162. package/.next/standalone/app/policies/hooks-client.tsx +113 -15
  163. package/.next/standalone/app/project/[name]/page.tsx +12 -2
  164. package/.next/standalone/integration-suite/README.md +43 -1
  165. package/.next/standalone/integration-suite/ci-entrypoint.sh +21 -6
  166. package/.next/standalone/integration-suite/install-clis.sh +58 -13
  167. package/.next/standalone/integration-suite/probe-cli.sh +51 -11
  168. package/.next/standalone/integration-suite/report.js +52 -6
  169. package/.next/standalone/integration-suite/run.sh +55 -3
  170. package/.next/standalone/lib/entry-keys.ts +36 -0
  171. package/.next/standalone/lib/hermes-profiles.ts +156 -0
  172. package/.next/standalone/lib/hermes-projects.ts +91 -25
  173. package/.next/standalone/lib/hermes-sessions.ts +65 -10
  174. package/.next/standalone/lib/openclaw-projects.ts +111 -18
  175. package/.next/standalone/lib/openclaw-sessions.ts +20 -0
  176. package/.next/standalone/lib/paths.ts +36 -0
  177. package/.next/standalone/lib/project-tree.ts +193 -0
  178. package/.next/standalone/lib/projects.ts +22 -0
  179. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  180. package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +3 -114
  181. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  182. package/.next/standalone/node_modules/next/dist/cli/next-test.js +2 -1
  183. package/.next/standalone/node_modules/next/dist/compiled/http-proxy/index.js +5 -5
  184. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +11 -10
  185. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -10
  186. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
  187. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +9 -9
  188. package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +1 -1
  189. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  190. package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
  191. package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +1 -1
  192. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
  193. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
  194. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +97 -56
  195. package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +129 -34
  196. package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +52 -4
  197. package/.next/standalone/node_modules/next/dist/server/config-schema.js +1 -1
  198. package/.next/standalone/node_modules/next/dist/server/config-shared.js +1 -2
  199. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  200. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  201. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  202. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +8 -27
  203. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  204. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +90 -53
  205. package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +15 -2
  206. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +4 -0
  207. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  208. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +1 -1
  209. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  210. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +2 -1
  211. package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +12 -0
  212. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  213. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  214. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  215. package/.next/standalone/node_modules/next/package.json +10 -10
  216. package/.next/standalone/package.json +9 -7
  217. package/.next/standalone/pi-extension/index.ts +38 -5
  218. package/.next/standalone/server.js +1 -1
  219. package/dist/cli.mjs +623 -308
  220. package/lib/entry-keys.ts +36 -0
  221. package/lib/hermes-profiles.ts +156 -0
  222. package/lib/hermes-projects.ts +91 -25
  223. package/lib/hermes-sessions.ts +65 -10
  224. package/lib/openclaw-projects.ts +111 -18
  225. package/lib/openclaw-sessions.ts +20 -0
  226. package/lib/paths.ts +36 -0
  227. package/lib/project-tree.ts +193 -0
  228. package/lib/projects.ts +22 -0
  229. package/package.json +9 -7
  230. package/pi-extension/index.ts +38 -5
  231. package/scripts/launch.ts +7 -0
  232. package/src/audit/cli-adapters/hermes.ts +6 -3
  233. package/src/audit/cli-adapters/openclaw.ts +17 -13
  234. package/src/hooks/configure-wizard.ts +24 -2
  235. package/src/hooks/custom-hooks-loader.ts +58 -4
  236. package/src/hooks/enforcement-capability.ts +266 -0
  237. package/src/hooks/handler.ts +12 -1
  238. package/src/hooks/hook-activity-store.ts +14 -0
  239. package/src/hooks/hooks-config.ts +59 -1
  240. package/src/hooks/integrations.ts +123 -19
  241. package/src/hooks/manager.ts +93 -27
  242. package/src/hooks/policy-evaluator.ts +59 -7
  243. package/src/hooks/policy-types.ts +26 -0
  244. package/src/hooks/types.ts +50 -2
  245. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0twi4fj._.js +0 -3
  253. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +0 -4
  254. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +0 -3
  255. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fax1sl._.js +0 -19
  256. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +0 -3
  260. package/.next/standalone/.next/static/chunks/0h-6ymo9v86b0.js +0 -6
  261. package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +0 -1
  262. package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +0 -1
  263. package/.next/standalone/.next/static/chunks/1bg_g3g359c_d.js +0 -1
  264. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +0 -2
  265. package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +0 -1
  266. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +0 -1
  267. /package/.next/standalone/.next/static/{YKxI22hC-F_nHp7MEKfb1 → QFmU1xD4R9N_HVfwccP6A}/_buildManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{YKxI22hC-F_nHp7MEKfb1 → QFmU1xD4R9N_HVfwccP6A}/_clientMiddlewareManifest.js +0 -0
  269. /package/.next/standalone/.next/static/{YKxI22hC-F_nHp7MEKfb1 → QFmU1xD4R9N_HVfwccP6A}/_ssgManifest.js +0 -0
@@ -7,7 +7,7 @@
7
7
  import { useState, useMemo, useEffect, useRef } from "react";
8
8
  import { ProjectFolder } from "@/lib/projects";
9
9
  import { CliBadge } from "./cli-badge";
10
- import { decodeFolderName } from "@/lib/paths";
10
+ import { projectDisplayName } from "@/lib/paths";
11
11
  import { formatDate } from "@/lib/format-date";
12
12
  import {
13
13
  FILTER_PRESETS,
@@ -24,7 +24,13 @@ import {
24
24
  pageToParam, paramToPage,
25
25
  } from "@/lib/url-filter-serializers";
26
26
  import { KNOWN_CLI_IDS, getCliLabel, isKnownCli, type CliId } from "@/lib/cli-registry";
27
- import { Folder, Search, X } from "lucide-react";
27
+ import {
28
+ buildProjectTree,
29
+ visibleTreeRows,
30
+ collapsibleKeys,
31
+ type ProjectTreeNode,
32
+ } from "@/lib/project-tree";
33
+ import { ChevronDown, ChevronRight, Folder, Search, X } from "lucide-react";
28
34
  import Link from "next/link";
29
35
  import PaginationControls from "./pagination-controls";
30
36
  import DatePickerInput from "./date-picker-input";
@@ -44,6 +50,126 @@ function normalizeKeywordForSearch(keyword: string): string {
44
50
  return keyword.trim().toLowerCase().replace(/\//g, "-");
45
51
  }
46
52
 
53
+ /** Where collapsed folder rows are remembered between visits. */
54
+ const COLLAPSED_STORAGE_KEY = "failproofai.projects.collapsed";
55
+
56
+ function readCollapsed(): string[] {
57
+ try {
58
+ const raw = window.localStorage.getItem(COLLAPSED_STORAGE_KEY);
59
+ const parsed = raw ? JSON.parse(raw) : null;
60
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
61
+ } catch {
62
+ // Private mode / disabled storage / corrupt value — everything stays expanded.
63
+ return [];
64
+ }
65
+ }
66
+
67
+ function persistCollapsed(keys: Set<string>): void {
68
+ try {
69
+ window.localStorage.setItem(COLLAPSED_STORAGE_KEY, JSON.stringify([...keys]));
70
+ } catch {
71
+ // Storage unavailable — collapsing still works for this session.
72
+ }
73
+ }
74
+
75
+ /**
76
+ * One row of the projects table — a leaf project, or a folder grouping the
77
+ * gateway CLIs' (profile/agent, channel) hierarchy.
78
+ *
79
+ * Both shapes render as a `<tr>` in the SAME table so the Path and Last
80
+ * Modified columns stay aligned down the page; depth becomes left padding on
81
+ * the name cell rather than a nested table.
82
+ */
83
+ function ProjectRow({
84
+ node,
85
+ collapsed,
86
+ interactive,
87
+ onToggle,
88
+ }: {
89
+ node: ProjectTreeNode;
90
+ collapsed: boolean;
91
+ /** False while a search force-expands the tree — see the folder branch below. */
92
+ interactive: boolean;
93
+ onToggle: (key: string) => void;
94
+ }) {
95
+ const isFolder = node.children.length > 0;
96
+ const project = node.project;
97
+ const Chevron = collapsed ? ChevronRight : ChevronDown;
98
+
99
+ // Depth is indentation, capped so a deep gateway path can't push the name off
100
+ // a narrow screen.
101
+ //
102
+ // Applied to the icon cell as well as the name, not just the name: the icon
103
+ // lives in its own <td>, so indenting only the name left every child's folder
104
+ // icon sitting at the same x as its parent's. The row then read as a flat
105
+ // list with ragged text rather than a hierarchy. Shifting both by the same
106
+ // amount moves icon and label together, so a child visibly starts to the
107
+ // right of its parent's icon.
108
+ const indent = { paddingLeft: `${Math.min(node.depth, 4) * 1.25}rem` };
109
+
110
+ return (
111
+ <tr className="border-b border-border hover:bg-muted/50 transition-colors">
112
+ <td className="px-4 py-3">
113
+ <div style={indent}>
114
+ <Folder className={`w-5 h-5 ${isFolder ? "text-muted-foreground" : "text-primary"}`} />
115
+ </div>
116
+ </td>
117
+ <td className="px-4 py-3 max-w-md">
118
+ <div className="flex flex-wrap items-center gap-2" style={indent}>
119
+ {isFolder ? (
120
+ // While a search is active the tree is force-expanded, so a toggle
121
+ // could only flip hidden state: it would look interactive, do
122
+ // nothing on screen, and silently invert what gets remembered once
123
+ // the search is cleared. Render it inert instead.
124
+ interactive ? (
125
+ <button
126
+ type="button"
127
+ onClick={() => onToggle(node.key)}
128
+ aria-expanded={!collapsed}
129
+ aria-label={`${collapsed ? "Expand" : "Collapse"} ${node.label}`}
130
+ className="flex items-center gap-1 font-semibold text-foreground hover:text-primary transition-colors break-words break-all text-left"
131
+ >
132
+ <Chevron className="w-4 h-4 shrink-0" />
133
+ {node.label}
134
+ </button>
135
+ ) : (
136
+ <span className="flex items-center gap-1 font-semibold text-foreground break-words break-all">
137
+ <Chevron className="w-4 h-4 shrink-0" />
138
+ {node.label}
139
+ </span>
140
+ )
141
+ ) : project ? (
142
+ <Link
143
+ href={`/project/${encodeURIComponent(project.name)}`}
144
+ className="font-semibold text-foreground hover:text-primary transition-colors break-words break-all inline-block max-w-full"
145
+ >
146
+ {/* Leaves of a gateway tree show only their own segment; a
147
+ filesystem project keeps its full decoded path. */}
148
+ {node.depth > 0 ? node.label : projectDisplayName(project.name, project.path)}
149
+ </Link>
150
+ ) : (
151
+ <span className="font-semibold text-foreground">{node.label}</span>
152
+ )}
153
+ {node.cli.map((c) => (
154
+ <CliBadge key={c} cli={c} />
155
+ ))}
156
+ {isFolder && node.sessionCount !== undefined && (
157
+ <span className="text-xs text-muted-foreground">
158
+ {node.sessionCount} session{node.sessionCount === 1 ? "" : "s"}
159
+ </span>
160
+ )}
161
+ </div>
162
+ </td>
163
+ <td className="px-4 py-3 text-sm text-muted-foreground hidden md:table-cell truncate max-w-md">
164
+ {project ? project.path : ""}
165
+ </td>
166
+ <td className="px-4 py-3 text-sm text-muted-foreground">
167
+ <DateDisplay date={node.lastModified} formatted={node.lastModifiedFormatted} />
168
+ </td>
169
+ </tr>
170
+ );
171
+ }
172
+
47
173
  export default function ProjectList({ folders }: ProjectListProps) {
48
174
  const url = useUrlParams();
49
175
  const mountedRef = useRef(false);
@@ -127,13 +253,88 @@ export default function ProjectList({ folders }: ProjectListProps) {
127
253
  return filtered.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
128
254
  }, [normalizedFolders, filterPreset, dateRange, keywords, filterCli]);
129
255
 
130
- const totalPages = Math.max(1, Math.ceil(filteredFolders.length / ITEMS_PER_PAGE));
256
+ // ── Folder tree ──
257
+ //
258
+ // Built from ALL filtered projects, never from a page slice: a gateway root's
259
+ // channels are individual projects interleaved by recency, so slicing first
260
+ // would split one profile across two pages — rendering the root twice, each
261
+ // time rolling up only the sessions that happened to land on that page, with
262
+ // the true total shown nowhere. Grouping is pure presentation; project names,
263
+ // and therefore every /project/<name> link, are untouched.
264
+ const tree = useMemo(() => buildProjectTree(filteredFolders), [filteredFolders]);
265
+
266
+ // Pagination walks TOP-LEVEL rows. With no gateway CLIs every root is exactly
267
+ // one project, so the page size and the reported range are identical to the
268
+ // flat table's; a gateway CLI simply folds its channels into one entry.
269
+ const totalPages = Math.max(1, Math.ceil(tree.length / ITEMS_PER_PAGE));
131
270
  useEffect(() => {
132
271
  if (currentPage > totalPages) setCurrentPage(totalPages);
133
272
  }, [currentPage, totalPages, setCurrentPage]);
134
- const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
135
- const endIndex = Math.min(startIndex + ITEMS_PER_PAGE, filteredFolders.length);
136
- const paginatedFolders = filteredFolders.slice(startIndex, endIndex);
273
+ const rootStart = (currentPage - 1) * ITEMS_PER_PAGE;
274
+ const rootEnd = Math.min(rootStart + ITEMS_PER_PAGE, tree.length);
275
+ const pageRoots = useMemo(() => tree.slice(rootStart, rootEnd), [tree, rootStart, rootEnd]);
276
+
277
+ // The summary counts PROJECTS, so derive its range from the projects the
278
+ // displayed roots actually cover rather than from the row indices.
279
+ const projectsBefore = tree
280
+ .slice(0, rootStart)
281
+ .reduce((sum, node) => sum + node.projectCount, 0);
282
+ const projectsOnPage = pageRoots.reduce((sum, node) => sum + node.projectCount, 0);
283
+ const startIndex = projectsBefore;
284
+ const endIndex = projectsBefore + projectsOnPage;
285
+
286
+ // Collapsed (not expanded) is what we persist: a profile or agent appearing
287
+ // for the first time should be visible, not hidden behind a click nobody
288
+ // knows to make. Loaded in an effect, never during render — reading
289
+ // localStorage while rendering would desync the server-rendered HTML.
290
+ const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set());
291
+ useEffect(() => {
292
+ setCollapsed(new Set(readCollapsed()));
293
+ }, []);
294
+
295
+ const toggleCollapsed = (key: string) => {
296
+ setCollapsed((prev) => {
297
+ const next = new Set(prev);
298
+ if (next.has(key)) next.delete(key);
299
+ else next.add(key);
300
+ persistCollapsed(next);
301
+ return next;
302
+ });
303
+ };
304
+
305
+ // A search that matched something must not leave the match hidden inside a
306
+ // collapsed folder, so an active search force-expands everything.
307
+ const searching = keywords.length > 0;
308
+ const visibleRows = useMemo(
309
+ () => visibleTreeRows(pageRoots, (key) => !searching && collapsed.has(key)),
310
+ [pageRoots, collapsed, searching],
311
+ );
312
+
313
+ // Keys the bulk toggle acts on: everything collapsible in the CURRENT view
314
+ // (this page, this filter). It must never be used to REPLACE the stored set —
315
+ // see toggleAll.
316
+ const pageCollapsibleKeys = useMemo(() => collapsibleKeys(pageRoots), [pageRoots]);
317
+ // The label, though, follows what is on SCREEN. Judging it by the whole page's
318
+ // keys would keep saying "Collapse all" after a root was collapsed, because
319
+ // its hidden descendants are still expanded — a button that then does nothing
320
+ // visible.
321
+ const visibleCollapsibleKeys = visibleRows.filter((n) => n.children.length > 0).map((n) => n.key);
322
+ const anyExpanded = visibleCollapsibleKeys.some((k) => !collapsed.has(k));
323
+
324
+ const toggleAll = () => {
325
+ setCollapsed((prev) => {
326
+ // MERGE, never replace: the stored set spans every page and filter, so
327
+ // assigning this view's keys wholesale would silently discard collapses
328
+ // the user made elsewhere.
329
+ const next = new Set(prev);
330
+ for (const key of pageCollapsibleKeys) {
331
+ if (anyExpanded) next.add(key);
332
+ else next.delete(key);
333
+ }
334
+ persistCollapsed(next);
335
+ return next;
336
+ });
337
+ };
137
338
 
138
339
  return (
139
340
  <div className="space-y-4">
@@ -266,11 +467,20 @@ export default function ProjectList({ folders }: ProjectListProps) {
266
467
  </div>
267
468
 
268
469
  {/* Results Count */}
269
- <div className="text-sm text-muted-foreground">
470
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground">
471
+ {pageCollapsibleKeys.length > 0 && !searching && (
472
+ <button
473
+ type="button"
474
+ onClick={toggleAll}
475
+ className="text-xs text-muted-foreground hover:text-foreground underline underline-offset-2 transition-colors"
476
+ >
477
+ {anyExpanded ? "Collapse all" : "Expand all"}
478
+ </button>
479
+ )}
270
480
  {filteredFolders.length === 0 ? (
271
- <>No projects found</>
481
+ <span>No projects found</span>
272
482
  ) : (
273
- <>
483
+ <span>
274
484
  Showing {startIndex + 1}-{endIndex} of {filteredFolders.length} projects
275
485
  {filteredFolders.length !== normalizedFolders.length && (
276
486
  <span className="ml-1">
@@ -282,7 +492,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
282
492
  with {keywords.length} keyword{keywords.length !== 1 ? "s" : ""}
283
493
  </span>
284
494
  )}
285
- </>
495
+ </span>
286
496
  )}
287
497
  </div>
288
498
  </div>
@@ -309,44 +519,21 @@ export default function ProjectList({ folders }: ProjectListProps) {
309
519
  </tr>
310
520
  </thead>
311
521
  <tbody>
312
- {paginatedFolders.length === 0 ? (
522
+ {visibleRows.length === 0 ? (
313
523
  <tr>
314
524
  <td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">
315
525
  No projects found matching the selected filter.
316
526
  </td>
317
527
  </tr>
318
528
  ) : (
319
- paginatedFolders.map((folder) => (
320
- <tr
321
- key={folder.name}
322
- className="border-b border-border hover:bg-muted/50 transition-colors"
323
- >
324
- <td className="px-4 py-3">
325
- <Folder className="w-5 h-5 text-primary" />
326
- </td>
327
- <td className="px-4 py-3 max-w-md">
328
- <div className="flex flex-wrap items-center gap-2">
329
- <Link
330
- href={`/project/${encodeURIComponent(folder.name)}`}
331
- className="font-semibold text-foreground hover:text-primary transition-colors break-words break-all inline-block max-w-full"
332
- >
333
- {decodeFolderName(folder.name)}
334
- </Link>
335
- {folder.cli.map((c) => (
336
- <CliBadge key={c} cli={c} />
337
- ))}
338
- </div>
339
- </td>
340
- <td className="px-4 py-3 text-sm text-muted-foreground hidden md:table-cell truncate max-w-md">
341
- {folder.path}
342
- </td>
343
- <td className="px-4 py-3 text-sm text-muted-foreground">
344
- <DateDisplay
345
- date={folder.lastModified}
346
- formatted={folder.lastModifiedFormatted}
347
- />
348
- </td>
349
- </tr>
529
+ visibleRows.map((node) => (
530
+ <ProjectRow
531
+ key={node.key}
532
+ node={node}
533
+ collapsed={!searching && collapsed.has(node.key)}
534
+ interactive={!searching}
535
+ onToggle={toggleCollapsed}
536
+ />
350
537
  ))
351
538
  )}
352
539
  </tbody>
@@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
9
9
  import { useWindowVirtualizer } from "@tanstack/react-virtual";
10
10
  import { ChevronDown, Wrench } from "lucide-react";
11
11
  import type { LogEntry, ToolUseBlock } from "@/lib/log-entries";
12
+ import { buildEntryKeys } from "@/lib/entry-keys";
12
13
  import { StatsBar } from "@/app/components/log-viewer/stats-bar";
13
14
  import { QueueDivider } from "@/app/components/log-viewer/queue-divider";
14
15
  import { EntryRow } from "@/app/components/log-viewer/entry-row";
@@ -123,8 +124,11 @@ function estimateSize(entry: LogEntry): number {
123
124
 
124
125
  type QueueOperationEntry = Extract<LogEntry, { type: "queue-operation" }>;
125
126
 
126
- function getSegmentId(entry: QueueOperationEntry): string {
127
- return `${entry.uuid}-${entry.timestampMs}`;
127
+ // Segment ids come from the same stable-key map as the rows, so uuid-less
128
+ // transcripts (Codex, Copilot, Cursor, Pi) can't collapse two dividers that
129
+ // happen to share a millisecond into one segment.
130
+ function getSegmentId(entry: QueueOperationEntry, keys: Map<LogEntry, string>): string {
131
+ return keys.get(entry) ?? `${entry.uuid}-${entry.timestampMs}`;
128
132
  }
129
133
 
130
134
  /**
@@ -132,7 +136,7 @@ function getSegmentId(entry: QueueOperationEntry): string {
132
136
  * to the count of non-queue-operation entries in its segment (entries after
133
137
  * it until the next queue-operation or end of list).
134
138
  */
135
- function computeSegments(entries: LogEntry[]): Map<string, number> {
139
+ function computeSegments(entries: LogEntry[], keys: Map<LogEntry, string>): Map<string, number> {
136
140
  const segments = new Map<string, number>();
137
141
  let currentId: string | null = null;
138
142
  let count = 0;
@@ -142,7 +146,7 @@ function computeSegments(entries: LogEntry[]): Map<string, number> {
142
146
  if (currentId !== null) {
143
147
  segments.set(currentId, count);
144
148
  }
145
- currentId = getSegmentId(entry);
149
+ currentId = getSegmentId(entry, keys);
146
150
  count = 0;
147
151
  } else if (currentId !== null) {
148
152
  count++;
@@ -154,11 +158,11 @@ function computeSegments(entries: LogEntry[]): Map<string, number> {
154
158
  return segments;
155
159
  }
156
160
 
157
- function filterVisibleEntries(entries: LogEntry[], collapsedSessions: Set<string>): LogEntry[] {
161
+ function filterVisibleEntries(entries: LogEntry[], collapsedSessions: Set<string>, keys: Map<LogEntry, string>): LogEntry[] {
158
162
  let currentCollapsed = false;
159
163
  return entries.filter((entry) => {
160
164
  if (entry.type === "queue-operation") {
161
- currentCollapsed = collapsedSessions.has(getSegmentId(entry as QueueOperationEntry));
165
+ currentCollapsed = collapsedSessions.has(getSegmentId(entry as QueueOperationEntry, keys));
162
166
  return true; // dividers are always visible
163
167
  }
164
168
  return !currentCollapsed;
@@ -234,11 +238,16 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId
234
238
  };
235
239
  }, []);
236
240
 
237
- const segments = useMemo(() => computeSegments(entries), [entries]);
241
+ // One identity per entry, shared by the React key, the segment ids and the
242
+ // virtualizer's measurement cache — all three must agree or rows get
243
+ // measured against the wrong entry.
244
+ const entryKeys = useMemo(() => buildEntryKeys(entries), [entries]);
245
+
246
+ const segments = useMemo(() => computeSegments(entries, entryKeys), [entries, entryKeys]);
238
247
 
239
248
  const visibleEntries = useMemo(
240
- () => filterVisibleEntries(entries, collapsedSessions),
241
- [entries, collapsedSessions],
249
+ () => filterVisibleEntries(entries, collapsedSessions, entryKeys),
250
+ [entries, collapsedSessions, entryKeys],
242
251
  );
243
252
 
244
253
  // UUID-to-index map for visible entries
@@ -303,14 +312,14 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId
303
312
  let currentSegmentId: string | null = null;
304
313
  for (const entry of entries) {
305
314
  if (entry.type === "queue-operation") {
306
- currentSegmentId = getSegmentId(entry);
315
+ currentSegmentId = getSegmentId(entry, entryKeys);
307
316
  } else if (entry.uuid === lookupUuid && currentSegmentId) {
308
317
  return currentSegmentId;
309
318
  }
310
319
  }
311
320
  }
312
321
  return null;
313
- }, [highlightedUuid, parentUuidForSubagent, entries, uuidToIndex]);
322
+ }, [highlightedUuid, parentUuidForSubagent, entries, uuidToIndex, entryKeys]);
314
323
 
315
324
  useEffect(() => {
316
325
  if (!segmentToExpand) return;
@@ -337,9 +346,18 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId
337
346
  });
338
347
  }, []);
339
348
 
349
+ // getItemKey must match the React key. Without it the virtualizer defaults to
350
+ // keying its size/element caches by index, so collapsing a segment shifts
351
+ // every index and replays one entry's cached height onto a different entry.
352
+ const getItemKey = useCallback(
353
+ (index: number) => entryKeys.get(visibleEntries[index]) ?? index,
354
+ [entryKeys, visibleEntries],
355
+ );
356
+
340
357
  const virtualizer = useWindowVirtualizer({
341
358
  count: visibleEntries.length,
342
359
  estimateSize: (index) => estimateSize(visibleEntries[index]),
360
+ getItemKey,
343
361
  overscan: 5,
344
362
  scrollMargin,
345
363
  });
@@ -370,7 +388,7 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId
370
388
  const isTarget = !!highlightedUuid && entry.uuid === (parentUuidForSubagent ?? highlightedUuid);
371
389
  return (
372
390
  <div
373
- key={entry.type === "queue-operation" ? getSegmentId(entry) : (entry.uuid || entry.timestamp)}
391
+ key={virtualRow.key}
374
392
  data-index={virtualRow.index}
375
393
  ref={virtualizer.measureElement}
376
394
  style={{
@@ -384,9 +402,9 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId
384
402
  {entry.type === "queue-operation" ? (
385
403
  <QueueDivider
386
404
  entry={entry}
387
- isCollapsed={collapsedSessions.has(getSegmentId(entry))}
388
- entryCount={segments.get(getSegmentId(entry)) ?? 0}
389
- onToggle={() => handleToggleSegment(getSegmentId(entry))}
405
+ isCollapsed={collapsedSessions.has(getSegmentId(entry, entryKeys))}
406
+ entryCount={segments.get(getSegmentId(entry, entryKeys)) ?? 0}
407
+ onToggle={() => handleToggleSegment(getSegmentId(entry, entryKeys))}
390
408
  />
391
409
  ) : (
392
410
  <EntryRow
@@ -3,18 +3,7 @@
3
3
  import React, { useState, useEffect, useCallback, useMemo, useRef, useTransition } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import Link from "next/link";
6
- import {
7
- ShieldCheck,
8
- ShieldX,
9
- ShieldAlert,
10
- Shield,
11
- ChevronDown,
12
- Copy,
13
- Check,
14
- Settings,
15
- Code,
16
- X,
17
- } from "lucide-react";
6
+ import { Check, ChevronDown, Code, Copy, Settings, Shield, ShieldAlert, ShieldCheck, ShieldX, TriangleAlert, X } from "lucide-react";
18
7
  import PaginationControls from "@/app/components/pagination-controls";
19
8
  import { getHookActivityAction, searchHookActivityAction } from "@/app/actions/get-hook-activity";
20
9
  import type { HookActivityPayload } from "@/app/actions/get-hook-activity";
@@ -29,6 +18,7 @@ import { usePostHog } from "@/contexts/PostHogContext";
29
18
  import { useUrlParams } from "@/lib/use-url-params";
30
19
  import { pageToParam, paramToPage } from "@/lib/url-filter-serializers";
31
20
  import { getCliLabel, getCliBadgeClasses, KNOWN_CLI_IDS, isKnownCli, type CliId } from "@/lib/cli-registry";
21
+ import { enforcementFor } from "@/src/hooks/enforcement-capability";
32
22
  import { formatRelativeTime } from "@/lib/format-duration";
33
23
  import { Button } from "@/components/ui/button";
34
24
 
@@ -330,6 +320,58 @@ function StatsBar({ stats }: { stats: HookActivityPayload["stats"] }) {
330
320
 
331
321
  // -- Expandable Detail Panel --
332
322
 
323
+
324
+ /**
325
+ * "Your policy ran, but this event cannot block on this CLI."
326
+ *
327
+ * Renders ONLY when the (cli, event) pair is a verified `observe`. A pair we
328
+ * have not traced returns undefined from `enforcementFor` and this renders
329
+ * nothing — a hedge ("capability unverified") would still be a claim, and
330
+ * unverified claims are the failure this whole matrix exists to prevent.
331
+ *
332
+ * The deny case matters more than the allow case: the row already says
333
+ * "denied", the activity store recorded a deny, and telemetry counted one — so
334
+ * without this note the UI actively reports enforcement that did not happen.
335
+ */
336
+ function EnforcementNote({
337
+ item,
338
+ }: {
339
+ item: HookActivityPayload["entries"][number];
340
+ }) {
341
+ const capability = enforcementFor(
342
+ item.integration as IntegrationType | undefined,
343
+ item.eventType,
344
+ );
345
+ if (capability !== "observe") return null;
346
+
347
+ const ran = item.matchedPolicies ?? [];
348
+ // Rows written before matchedPolicies existed have no list; naming no policy
349
+ // is better than implying none ran.
350
+ if (ran.length === 0) return null;
351
+
352
+ const cli = item.integration ? getCliLabel(item.integration as IntegrationType) : "This CLI";
353
+ const names = ran.map((n) => n.replace(/^(custom|\.failproofai-(project|user))\//, ""));
354
+ const label = names.length === 1 ? names[0] : `${names.length} policies`;
355
+ const denied = item.decision === "deny";
356
+
357
+ return (
358
+ <div className="sm:col-span-2 lg:col-span-3 mt-1 flex items-start gap-2 rounded border border-amber-500/30 bg-amber-500/5 px-2.5 py-2">
359
+ <TriangleAlert className="h-3.5 w-3.5 shrink-0 text-amber-500/80 mt-0.5" aria-hidden="true" />
360
+ <p className="text-[0.7rem] leading-relaxed text-muted-foreground">
361
+ <span className="font-medium text-foreground">
362
+ {denied ? "Not enforced." : "Ran, but cannot enforce here."}
363
+ </span>{" "}
364
+ <span className="font-mono">{label}</span> ran on{" "}
365
+ <span className="font-mono">{item.eventType}</span>, but {cli} discards hook
366
+ verdicts for this event
367
+ {denied ? " — the action went ahead anyway." : ", so a policy cannot block here."}{" "}
368
+ Move the check to an event {cli} enforces on, such as{" "}
369
+ <span className="font-mono">PreToolUse</span>.
370
+ </p>
371
+ </div>
372
+ );
373
+ }
374
+
333
375
  function DetailPanel({
334
376
  item,
335
377
  }: {
@@ -344,6 +386,7 @@ function DetailPanel({
344
386
  event detail
345
387
  </span>
346
388
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-2">
389
+ <EnforcementNote item={item} />
347
390
  <div>
348
391
  <span className="text-muted-foreground">Session ID: </span>
349
392
  <span className="font-mono text-foreground">
@@ -575,7 +618,7 @@ function ActivityTab({
575
618
  type="text"
576
619
  value={filterPolicy}
577
620
  onChange={(e) => setFilterPolicy(e.target.value)}
578
- placeholder="filter by policy\u2026"
621
+ placeholder="filter by policy…"
579
622
  className="filter-input"
580
623
  />
581
624
  </div>
@@ -1427,7 +1470,7 @@ function PoliciesTab({ onHooksInstallChange }: { onHooksInstallChange?: (install
1427
1470
  <span className="text-xs text-muted-foreground">
1428
1471
  <span className="font-semibold text-foreground">{config.enabledPolicies.length}</span>
1429
1472
  {" / "}
1430
- {config.policies.length + (config.customPolicies?.length ?? 0)}{" "}
1473
+ {config.policies.length + (config.customPolicies?.length ?? 0) + (config.conventionPolicies?.reduce((n, e) => n + e.policies.length, 0) ?? 0)}{" "}
1431
1474
  policies enabled
1432
1475
  </span>
1433
1476
  {installed && (
@@ -1596,6 +1639,58 @@ function PoliciesTab({ onHooksInstallChange }: { onHooksInstallChange?: (install
1596
1639
  ))}
1597
1640
  </div>
1598
1641
  )}
1642
+
1643
+ {/* Convention policies — discovered from .failproofai/policies/, never
1644
+ from config, so they are grouped by the file that declares them. */}
1645
+ {config.conventionPolicies?.map((entry) => (
1646
+ <div key={entry.path}>
1647
+ <div className="flex items-center justify-between px-4 py-2.5 bg-muted/20 border-b border-border/50">
1648
+ <span className="text-[0.7rem] font-semibold uppercase tracking-wider text-muted-foreground">
1649
+ Convention Policies — {entry.scope === "project" ? "Project" : "User"}
1650
+ </span>
1651
+ <span className="text-[0.7rem] text-muted-foreground">
1652
+ {entry.policies.length} hook{entry.policies.length === 1 ? "" : "s"}
1653
+ </span>
1654
+ </div>
1655
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border/20">
1656
+ <Code className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
1657
+ <span className="text-xs font-mono text-muted-foreground truncate">{entry.path}</span>
1658
+ </div>
1659
+ {entry.policies.length === 0 ? (
1660
+ <div className="flex items-start gap-2 px-4 py-2.5 border-b border-border/20 bg-muted/10">
1661
+ <Shield className="h-3.5 w-3.5 text-muted-foreground/60 shrink-0 mt-0.5" />
1662
+ <p className="text-[0.7rem] text-muted-foreground/70 leading-relaxed">
1663
+ No policies could be read from this file. It is still loaded at runtime —
1664
+ run <span className="font-mono">failproofai policies</span> to see what it registers.
1665
+ </p>
1666
+ </div>
1667
+ ) : (
1668
+ entry.policies.map((policy) => (
1669
+ <div
1670
+ key={`${entry.path}:${policy.name}`}
1671
+ className="flex items-start gap-3 px-4 py-3 border-b border-border/20 hover:bg-muted/20 transition-colors"
1672
+ >
1673
+ <div className="h-4 w-7 shrink-0 mt-0.5" />
1674
+ <div className="flex items-center gap-1.5 min-w-0 w-56 shrink-0 mt-0.5">
1675
+ <span className="text-xs font-mono text-foreground truncate">{policy.name}</span>
1676
+ </div>
1677
+ <div className="flex-1 min-w-0">
1678
+ {policy.description && (
1679
+ <span className="text-xs text-muted-foreground leading-relaxed">
1680
+ {policy.description}
1681
+ </span>
1682
+ )}
1683
+ {policy.eventScope && (
1684
+ <span className="block text-[0.65rem] text-muted-foreground/40 font-mono mt-0.5 hidden lg:block">
1685
+ {policy.eventScope}
1686
+ </span>
1687
+ )}
1688
+ </div>
1689
+ </div>
1690
+ ))
1691
+ )}
1692
+ </div>
1693
+ ))}
1599
1694
  </div>
1600
1695
  </>
1601
1696
  );
@@ -1646,7 +1741,10 @@ export default function HooksClient({ initialTab = "activity" }: { initialTab?:
1646
1741
  setHooksInstalled(cfg.clis.some((c) => c.installed));
1647
1742
  setPolicyCounts({
1648
1743
  enabled: cfg.enabledPolicies.length,
1649
- total: cfg.policies.length + (cfg.customPolicies?.length ?? 0),
1744
+ total:
1745
+ cfg.policies.length +
1746
+ (cfg.customPolicies?.length ?? 0) +
1747
+ (cfg.conventionPolicies?.reduce((n, e) => n + e.policies.length, 0) ?? 0),
1650
1748
  });
1651
1749
  setInstalledCliLabels(cfg.clis.filter((c) => c.installed).map((c) => c.label));
1652
1750
  })
@@ -13,7 +13,7 @@ import { getCachedDevinSessionsByEncodedName } from "@/lib/devin-projects";
13
13
  import { getCachedAntigravitySessionsByEncodedName } from "@/lib/antigravity-projects";
14
14
  import { getCachedGooseSessionsByEncodedName } from "@/lib/goose-projects";
15
15
  import { logWarn } from "@/lib/logger";
16
- import { decodeFolderName } from "@/lib/paths";
16
+ import { decodeFolderName, projectDisplayName, isSyntheticProjectPath } from "@/lib/paths";
17
17
  import { notFound } from "next/navigation";
18
18
  import { existsSync } from "fs";
19
19
  import { stat } from "fs/promises";
@@ -150,7 +150,17 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
150
150
  back to projects
151
151
  </Link>
152
152
 
153
- <h1 className="project-page-title">{canonicalRoot}</h1>
153
+ {/* Gateway projects carry a synthetic `hermes:profile:source` root; show
154
+ the same slash form the projects panel does, so the page you land on
155
+ is titled what you clicked. Everything else keeps the LITERAL cwd
156
+ recovered above — passing it through projectDisplayName would discard
157
+ it and re-derive a lossy name from the slug, turning a real
158
+ `/home/u/my-app` into `/home/u/my/app`. */}
159
+ <h1 className="project-page-title">
160
+ {isSyntheticProjectPath(canonicalRoot)
161
+ ? projectDisplayName(name, canonicalRoot)
162
+ : canonicalRoot}
163
+ </h1>
154
164
 
155
165
  <div className="project-page-meta">
156
166
  <span className="label">path</span>