@penclipai/server 2026.704.0 → 2026.714.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 (485) hide show
  1. package/dist/adapters/index.d.ts +1 -1
  2. package/dist/adapters/index.d.ts.map +1 -1
  3. package/dist/adapters/index.js.map +1 -1
  4. package/dist/adapters/process/execute.d.ts.map +1 -1
  5. package/dist/adapters/process/execute.js +1 -0
  6. package/dist/adapters/process/execute.js.map +1 -1
  7. package/dist/adapters/registry.d.ts.map +1 -1
  8. package/dist/adapters/registry.js +79 -51
  9. package/dist/adapters/registry.js.map +1 -1
  10. package/dist/adapters/utils.d.ts +5 -0
  11. package/dist/adapters/utils.d.ts.map +1 -1
  12. package/dist/adapters/utils.js.map +1 -1
  13. package/dist/agent-auth-jwt.d.ts +5 -1
  14. package/dist/agent-auth-jwt.d.ts.map +1 -1
  15. package/dist/agent-auth-jwt.js +72 -20
  16. package/dist/agent-auth-jwt.js.map +1 -1
  17. package/dist/app.d.ts +2 -0
  18. package/dist/app.d.ts.map +1 -1
  19. package/dist/app.js +10 -0
  20. package/dist/app.js.map +1 -1
  21. package/dist/built-ins/agents/reflection-coach/AGENTS.md +47 -0
  22. package/dist/built-ins/agents/reflection-coach/routines/recent-agent-reflection.md +77 -0
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +2 -2
  26. package/dist/errors.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +208 -116
  29. package/dist/index.js.map +1 -1
  30. package/dist/middleware/api-compression.d.ts +8 -0
  31. package/dist/middleware/api-compression.d.ts.map +1 -0
  32. package/dist/middleware/api-compression.js +207 -0
  33. package/dist/middleware/api-compression.js.map +1 -0
  34. package/dist/middleware/auth.d.ts.map +1 -1
  35. package/dist/middleware/auth.js +130 -3
  36. package/dist/middleware/auth.js.map +1 -1
  37. package/dist/middleware/error-handler.d.ts.map +1 -1
  38. package/dist/middleware/error-handler.js +27 -0
  39. package/dist/middleware/error-handler.js.map +1 -1
  40. package/dist/onboarding-assets/default/AGENTS.md +1 -0
  41. package/dist/redaction.d.ts.map +1 -1
  42. package/dist/redaction.js +11 -0
  43. package/dist/redaction.js.map +1 -1
  44. package/dist/routes/access.d.ts.map +1 -1
  45. package/dist/routes/access.js +4 -3
  46. package/dist/routes/access.js.map +1 -1
  47. package/dist/routes/adapters.d.ts.map +1 -1
  48. package/dist/routes/adapters.js +2 -0
  49. package/dist/routes/adapters.js.map +1 -1
  50. package/dist/routes/agents.d.ts.map +1 -1
  51. package/dist/routes/agents.js +222 -87
  52. package/dist/routes/agents.js.map +1 -1
  53. package/dist/routes/attention.d.ts +3 -0
  54. package/dist/routes/attention.d.ts.map +1 -0
  55. package/dist/routes/attention.js +24 -0
  56. package/dist/routes/attention.js.map +1 -0
  57. package/dist/routes/authz.d.ts.map +1 -1
  58. package/dist/routes/authz.js +29 -1
  59. package/dist/routes/authz.js.map +1 -1
  60. package/dist/routes/board-chat.d.ts.map +1 -1
  61. package/dist/routes/board-chat.js +4 -1
  62. package/dist/routes/board-chat.js.map +1 -1
  63. package/dist/routes/built-in-agents.d.ts +3 -0
  64. package/dist/routes/built-in-agents.d.ts.map +1 -0
  65. package/dist/routes/built-in-agents.js +267 -0
  66. package/dist/routes/built-in-agents.js.map +1 -0
  67. package/dist/routes/cases.d.ts +46 -0
  68. package/dist/routes/cases.d.ts.map +1 -0
  69. package/dist/routes/cases.js +1395 -0
  70. package/dist/routes/cases.js.map +1 -0
  71. package/dist/routes/companies.d.ts.map +1 -1
  72. package/dist/routes/companies.js +4 -1
  73. package/dist/routes/companies.js.map +1 -1
  74. package/dist/routes/company-skills.d.ts.map +1 -1
  75. package/dist/routes/company-skills.js +453 -33
  76. package/dist/routes/company-skills.js.map +1 -1
  77. package/dist/routes/environments.d.ts.map +1 -1
  78. package/dist/routes/environments.js +71 -8
  79. package/dist/routes/environments.js.map +1 -1
  80. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  81. package/dist/routes/execution-workspaces.js +89 -3
  82. package/dist/routes/execution-workspaces.js.map +1 -1
  83. package/dist/routes/health.d.ts +2 -0
  84. package/dist/routes/health.d.ts.map +1 -1
  85. package/dist/routes/health.js +30 -0
  86. package/dist/routes/health.js.map +1 -1
  87. package/dist/routes/inbox-dismissals.d.ts.map +1 -1
  88. package/dist/routes/inbox-dismissals.js +80 -19
  89. package/dist/routes/inbox-dismissals.js.map +1 -1
  90. package/dist/routes/index.d.ts +2 -0
  91. package/dist/routes/index.d.ts.map +1 -1
  92. package/dist/routes/index.js +2 -0
  93. package/dist/routes/index.js.map +1 -1
  94. package/dist/routes/issues.d.ts +25 -0
  95. package/dist/routes/issues.d.ts.map +1 -1
  96. package/dist/routes/issues.js +1455 -46
  97. package/dist/routes/issues.js.map +1 -1
  98. package/dist/routes/openapi.d.ts.map +1 -1
  99. package/dist/routes/openapi.js +448 -11
  100. package/dist/routes/openapi.js.map +1 -1
  101. package/dist/routes/pipelines.js +2 -2
  102. package/dist/routes/pipelines.js.map +1 -1
  103. package/dist/routes/resource-memberships.d.ts.map +1 -1
  104. package/dist/routes/resource-memberships.js +13 -5
  105. package/dist/routes/resource-memberships.js.map +1 -1
  106. package/dist/routes/secrets.d.ts.map +1 -1
  107. package/dist/routes/secrets.js +261 -1
  108. package/dist/routes/secrets.js.map +1 -1
  109. package/dist/routes/sidebar-badges.d.ts.map +1 -1
  110. package/dist/routes/sidebar-badges.js +18 -2
  111. package/dist/routes/sidebar-badges.js.map +1 -1
  112. package/dist/routes/user-profiles.d.ts.map +1 -1
  113. package/dist/routes/user-profiles.js +5 -4
  114. package/dist/routes/user-profiles.js.map +1 -1
  115. package/dist/server-info.d.ts +2 -0
  116. package/dist/server-info.d.ts.map +1 -1
  117. package/dist/server-info.js +20 -5
  118. package/dist/server-info.js.map +1 -1
  119. package/dist/services/access.d.ts +4 -4
  120. package/dist/services/access.d.ts.map +1 -1
  121. package/dist/services/activity.d.ts +3 -3
  122. package/dist/services/activity.d.ts.map +1 -1
  123. package/dist/services/activity.js +5 -4
  124. package/dist/services/activity.js.map +1 -1
  125. package/dist/services/adapter-registry-bootstrap.reconcile.test.js +1 -0
  126. package/dist/services/adapter-registry-bootstrap.reconcile.test.js.map +1 -1
  127. package/dist/services/agent-secret-bindings.d.ts +15 -0
  128. package/dist/services/agent-secret-bindings.d.ts.map +1 -1
  129. package/dist/services/agent-secret-bindings.js +43 -0
  130. package/dist/services/agent-secret-bindings.js.map +1 -1
  131. package/dist/services/agents.d.ts +194 -133
  132. package/dist/services/agents.d.ts.map +1 -1
  133. package/dist/services/agents.js +107 -4
  134. package/dist/services/agents.js.map +1 -1
  135. package/dist/services/approvals.d.ts +10 -10
  136. package/dist/services/approvals.d.ts.map +1 -1
  137. package/dist/services/approvals.js +9 -1
  138. package/dist/services/approvals.js.map +1 -1
  139. package/dist/services/assets.d.ts +5 -5
  140. package/dist/services/attention.d.ts +11 -0
  141. package/dist/services/attention.d.ts.map +1 -0
  142. package/dist/services/attention.js +1024 -0
  143. package/dist/services/attention.js.map +1 -0
  144. package/dist/services/authorization.d.ts +15 -2
  145. package/dist/services/authorization.d.ts.map +1 -1
  146. package/dist/services/authorization.js +324 -8
  147. package/dist/services/authorization.js.map +1 -1
  148. package/dist/services/board-auth.d.ts +2 -2
  149. package/dist/services/budgets.d.ts.map +1 -1
  150. package/dist/services/budgets.js +8 -7
  151. package/dist/services/budgets.js.map +1 -1
  152. package/dist/services/built-in-agent-metadata.d.ts +9 -0
  153. package/dist/services/built-in-agent-metadata.d.ts.map +1 -0
  154. package/dist/services/built-in-agent-metadata.js +39 -0
  155. package/dist/services/built-in-agent-metadata.js.map +1 -0
  156. package/dist/services/built-in-agents.d.ts +394 -0
  157. package/dist/services/built-in-agents.d.ts.map +1 -0
  158. package/dist/services/built-in-agents.js +1426 -0
  159. package/dist/services/built-in-agents.js.map +1 -0
  160. package/dist/services/change-consent-gate.d.ts +18 -0
  161. package/dist/services/change-consent-gate.d.ts.map +1 -0
  162. package/dist/services/change-consent-gate.js +170 -0
  163. package/dist/services/change-consent-gate.js.map +1 -0
  164. package/dist/services/companies.d.ts +14 -8
  165. package/dist/services/companies.d.ts.map +1 -1
  166. package/dist/services/companies.js +6 -1
  167. package/dist/services/companies.js.map +1 -1
  168. package/dist/services/company-artifacts.d.ts +4 -1
  169. package/dist/services/company-artifacts.d.ts.map +1 -1
  170. package/dist/services/company-artifacts.js +9 -1
  171. package/dist/services/company-artifacts.js.map +1 -1
  172. package/dist/services/company-member-roles.d.ts.map +1 -1
  173. package/dist/services/company-member-roles.js +2 -0
  174. package/dist/services/company-member-roles.js.map +1 -1
  175. package/dist/services/company-portability.d.ts +1 -1
  176. package/dist/services/company-portability.d.ts.map +1 -1
  177. package/dist/services/company-portability.js +113 -8
  178. package/dist/services/company-portability.js.map +1 -1
  179. package/dist/services/company-search.d.ts.map +1 -1
  180. package/dist/services/company-search.js +843 -294
  181. package/dist/services/company-search.js.map +1 -1
  182. package/dist/services/company-skills.d.ts +64 -3
  183. package/dist/services/company-skills.d.ts.map +1 -1
  184. package/dist/services/company-skills.js +1205 -20
  185. package/dist/services/company-skills.js.map +1 -1
  186. package/dist/services/costs.d.ts +9 -8
  187. package/dist/services/costs.d.ts.map +1 -1
  188. package/dist/services/costs.js +9 -2
  189. package/dist/services/costs.js.map +1 -1
  190. package/dist/services/dashboard.d.ts +2 -0
  191. package/dist/services/dashboard.d.ts.map +1 -1
  192. package/dist/services/dashboard.js +60 -17
  193. package/dist/services/dashboard.js.map +1 -1
  194. package/dist/services/database-backup-health.d.ts +34 -0
  195. package/dist/services/database-backup-health.d.ts.map +1 -0
  196. package/dist/services/database-backup-health.js +104 -0
  197. package/dist/services/database-backup-health.js.map +1 -0
  198. package/dist/services/document-annotations.d.ts +169 -1
  199. package/dist/services/document-annotations.d.ts.map +1 -1
  200. package/dist/services/document-annotations.js +272 -1
  201. package/dist/services/document-annotations.js.map +1 -1
  202. package/dist/services/documents.d.ts +368 -0
  203. package/dist/services/documents.d.ts.map +1 -1
  204. package/dist/services/documents.js +2 -2
  205. package/dist/services/documents.js.map +1 -1
  206. package/dist/services/environment-config.d.ts.map +1 -1
  207. package/dist/services/environment-config.js +6 -0
  208. package/dist/services/environment-config.js.map +1 -1
  209. package/dist/services/environment-custom-image-runtime.d.ts +28 -0
  210. package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
  211. package/dist/services/environment-custom-image-runtime.js +106 -7
  212. package/dist/services/environment-custom-image-runtime.js.map +1 -1
  213. package/dist/services/environment-custom-images.d.ts +31 -1
  214. package/dist/services/environment-custom-images.d.ts.map +1 -1
  215. package/dist/services/environment-custom-images.js +110 -4
  216. package/dist/services/environment-custom-images.js.map +1 -1
  217. package/dist/services/environment-execution-target.d.ts.map +1 -1
  218. package/dist/services/environment-execution-target.js +1 -3
  219. package/dist/services/environment-execution-target.js.map +1 -1
  220. package/dist/services/environment-probe.d.ts +1 -0
  221. package/dist/services/environment-probe.d.ts.map +1 -1
  222. package/dist/services/environment-probe.js +99 -5
  223. package/dist/services/environment-probe.js.map +1 -1
  224. package/dist/services/environments.d.ts +3 -1
  225. package/dist/services/environments.d.ts.map +1 -1
  226. package/dist/services/environments.js +98 -3
  227. package/dist/services/environments.js.map +1 -1
  228. package/dist/services/execution-workspace-policy.d.ts +29 -3
  229. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  230. package/dist/services/execution-workspace-policy.js +44 -1
  231. package/dist/services/execution-workspace-policy.js.map +1 -1
  232. package/dist/services/execution-workspaces.d.ts +64 -1
  233. package/dist/services/execution-workspaces.d.ts.map +1 -1
  234. package/dist/services/execution-workspaces.js +634 -3
  235. package/dist/services/execution-workspaces.js.map +1 -1
  236. package/dist/services/external-objects.d.ts +26 -26
  237. package/dist/services/feedback.d.ts +2 -2
  238. package/dist/services/finance.d.ts +5 -5
  239. package/dist/services/goals.d.ts +8 -8
  240. package/dist/services/heartbeat-stop-metadata.d.ts +2 -2
  241. package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
  242. package/dist/services/heartbeat-stop-metadata.js +4 -0
  243. package/dist/services/heartbeat-stop-metadata.js.map +1 -1
  244. package/dist/services/heartbeat-stop-metadata.test.js +9 -0
  245. package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
  246. package/dist/services/heartbeat.d.ts +133 -28
  247. package/dist/services/heartbeat.d.ts.map +1 -1
  248. package/dist/services/heartbeat.js +1963 -239
  249. package/dist/services/heartbeat.js.map +1 -1
  250. package/dist/services/inbox-dismissals.d.ts +26 -0
  251. package/dist/services/inbox-dismissals.d.ts.map +1 -1
  252. package/dist/services/inbox-dismissals.js +33 -18
  253. package/dist/services/inbox-dismissals.js.map +1 -1
  254. package/dist/services/index.d.ts +3 -1
  255. package/dist/services/index.d.ts.map +1 -1
  256. package/dist/services/index.js +3 -1
  257. package/dist/services/index.js.map +1 -1
  258. package/dist/services/instance-settings.d.ts +25 -1
  259. package/dist/services/instance-settings.d.ts.map +1 -1
  260. package/dist/services/instance-settings.js +103 -5
  261. package/dist/services/instance-settings.js.map +1 -1
  262. package/dist/services/issue-continuation-summary.js +1 -1
  263. package/dist/services/issue-continuation-summary.js.map +1 -1
  264. package/dist/services/issue-recovery-actions.d.ts +3 -0
  265. package/dist/services/issue-recovery-actions.d.ts.map +1 -1
  266. package/dist/services/issue-recovery-actions.js +9 -0
  267. package/dist/services/issue-recovery-actions.js.map +1 -1
  268. package/dist/services/issue-rewake-throttle.d.ts +92 -0
  269. package/dist/services/issue-rewake-throttle.d.ts.map +1 -0
  270. package/dist/services/issue-rewake-throttle.js +139 -0
  271. package/dist/services/issue-rewake-throttle.js.map +1 -0
  272. package/dist/services/issue-thread-interactions.d.ts +8 -1
  273. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  274. package/dist/services/issue-thread-interactions.js +231 -16
  275. package/dist/services/issue-thread-interactions.js.map +1 -1
  276. package/dist/services/issue-visibility.d.ts +4 -0
  277. package/dist/services/issue-visibility.d.ts.map +1 -0
  278. package/dist/services/issue-visibility.js +9 -0
  279. package/dist/services/issue-visibility.js.map +1 -0
  280. package/dist/services/issues.d.ts +225 -105
  281. package/dist/services/issues.d.ts.map +1 -1
  282. package/dist/services/issues.js +540 -18
  283. package/dist/services/issues.js.map +1 -1
  284. package/dist/services/local-service-supervisor.d.ts +3 -0
  285. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  286. package/dist/services/local-service-supervisor.js +51 -0
  287. package/dist/services/local-service-supervisor.js.map +1 -1
  288. package/dist/services/pipeline-case-outputs.d.ts.map +1 -1
  289. package/dist/services/pipeline-case-outputs.js +2 -1
  290. package/dist/services/pipeline-case-outputs.js.map +1 -1
  291. package/dist/services/pipelines-aggregation.d.ts.map +1 -1
  292. package/dist/services/pipelines-aggregation.js +3 -2
  293. package/dist/services/pipelines-aggregation.js.map +1 -1
  294. package/dist/services/pipelines.d.ts +165 -165
  295. package/dist/services/pipelines.d.ts.map +1 -1
  296. package/dist/services/pipelines.js +6 -4
  297. package/dist/services/pipelines.js.map +1 -1
  298. package/dist/services/plugin-database.d.ts +2 -2
  299. package/dist/services/plugin-environment-driver.d.ts +1 -1
  300. package/dist/services/plugin-host-services.d.ts.map +1 -1
  301. package/dist/services/plugin-host-services.js +3 -1
  302. package/dist/services/plugin-host-services.js.map +1 -1
  303. package/dist/services/plugin-managed-routines.d.ts +1 -0
  304. package/dist/services/plugin-managed-routines.d.ts.map +1 -1
  305. package/dist/services/plugin-registry.d.ts +21 -21
  306. package/dist/services/productivity-review.d.ts +1 -0
  307. package/dist/services/productivity-review.d.ts.map +1 -1
  308. package/dist/services/productivity-review.js +6 -5
  309. package/dist/services/productivity-review.js.map +1 -1
  310. package/dist/services/projects.d.ts +9 -9
  311. package/dist/services/recovery/service.d.ts +82 -54
  312. package/dist/services/recovery/service.d.ts.map +1 -1
  313. package/dist/services/recovery/service.js +200 -35
  314. package/dist/services/recovery/service.js.map +1 -1
  315. package/dist/services/recovery/successful-run-handoff.d.ts +1 -0
  316. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  317. package/dist/services/recovery/successful-run-handoff.js +2 -0
  318. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  319. package/dist/services/recovery/successful-run-handoff.test.js +20 -0
  320. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  321. package/dist/services/resource-memberships.d.ts +13 -10
  322. package/dist/services/resource-memberships.d.ts.map +1 -1
  323. package/dist/services/resource-memberships.js +89 -20
  324. package/dist/services/resource-memberships.js.map +1 -1
  325. package/dist/services/responsible-user-denial-run-outcomes.d.ts +60 -0
  326. package/dist/services/responsible-user-denial-run-outcomes.d.ts.map +1 -0
  327. package/dist/services/responsible-user-denial-run-outcomes.js +56 -0
  328. package/dist/services/responsible-user-denial-run-outcomes.js.map +1 -0
  329. package/dist/services/responsible-user-denial-run-outcomes.test.d.ts +2 -0
  330. package/dist/services/responsible-user-denial-run-outcomes.test.d.ts.map +1 -0
  331. package/dist/services/responsible-user-denial-run-outcomes.test.js +75 -0
  332. package/dist/services/responsible-user-denial-run-outcomes.test.js.map +1 -0
  333. package/dist/services/routines.d.ts +30 -8
  334. package/dist/services/routines.d.ts.map +1 -1
  335. package/dist/services/routines.js +241 -18
  336. package/dist/services/routines.js.map +1 -1
  337. package/dist/services/run-liveness.d.ts.map +1 -1
  338. package/dist/services/run-liveness.js +21 -0
  339. package/dist/services/run-liveness.js.map +1 -1
  340. package/dist/services/run-log-store.d.ts +1 -0
  341. package/dist/services/run-log-store.d.ts.map +1 -1
  342. package/dist/services/run-log-store.js +4 -0
  343. package/dist/services/run-log-store.js.map +1 -1
  344. package/dist/services/run-scratch.d.ts +42 -0
  345. package/dist/services/run-scratch.d.ts.map +1 -0
  346. package/dist/services/run-scratch.js +111 -0
  347. package/dist/services/run-scratch.js.map +1 -0
  348. package/dist/services/run-scratch.test.d.ts +2 -0
  349. package/dist/services/run-scratch.test.d.ts.map +1 -0
  350. package/dist/services/run-scratch.test.js +98 -0
  351. package/dist/services/run-scratch.test.js.map +1 -0
  352. package/dist/services/secrets.d.ts +1261 -64
  353. package/dist/services/secrets.d.ts.map +1 -1
  354. package/dist/services/secrets.js +1089 -96
  355. package/dist/services/secrets.js.map +1 -1
  356. package/dist/services/task-watchdogs.d.ts +1 -0
  357. package/dist/services/task-watchdogs.d.ts.map +1 -1
  358. package/dist/services/task-watchdogs.js +78 -9
  359. package/dist/services/task-watchdogs.js.map +1 -1
  360. package/dist/services/work-timeline.d.ts.map +1 -1
  361. package/dist/services/work-timeline.js +34 -2
  362. package/dist/services/work-timeline.js.map +1 -1
  363. package/dist/services/workspace-runtime-read-model.d.ts +30 -29
  364. package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
  365. package/dist/services/workspace-runtime-read-model.js.map +1 -1
  366. package/dist/services/workspace-runtime.d.ts +59 -13
  367. package/dist/services/workspace-runtime.d.ts.map +1 -1
  368. package/dist/services/workspace-runtime.js +1121 -102
  369. package/dist/services/workspace-runtime.js.map +1 -1
  370. package/dist/skills-catalog/catalog/bundled/paperclip-operations/reflection-coach/SKILL.md +202 -0
  371. package/dist/skills-catalog/catalog/bundled/product/paperclip-capsules/SKILL.md +1 -1
  372. package/dist/skills-catalog/catalog/bundled/product/wireframe/SKILL.md +1 -1
  373. package/dist/skills-catalog/catalog/optional/content/release-announcement/SKILL.md +90 -0
  374. package/dist/skills-catalog/catalog/optional/finance/ramp/SKILL.md +98 -0
  375. package/dist/skills-catalog/generated/catalog.json +84 -12
  376. package/dist/ui-branding.d.ts +7 -0
  377. package/dist/ui-branding.d.ts.map +1 -1
  378. package/dist/ui-branding.js +8 -2
  379. package/dist/ui-branding.js.map +1 -1
  380. package/dist/version.d.ts +14 -0
  381. package/dist/version.d.ts.map +1 -1
  382. package/dist/version.js +114 -3
  383. package/dist/version.js.map +1 -1
  384. package/package.json +19 -20
  385. package/skills/paperclip/SKILL.md +45 -10
  386. package/skills/paperclip/references/api-reference.md +246 -1
  387. package/skills/paperclip/references/cases.md +295 -0
  388. package/skills/paperclip/references/company-skills.md +1 -1
  389. package/skills/paperclip-board/SKILL.md +3 -4
  390. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +3 -7
  391. package/skills/para-memory-files/SKILL.md +3 -7
  392. package/ui-dist/assets/{abnfDiagram-VRR7QNED-BsFz2IiG.js → abnfDiagram-VRR7QNED-Dtgfy1PK.js} +1 -1
  393. package/ui-dist/assets/{arc-HZhAwbOs.js → arc-BGdvNvs8.js} +1 -1
  394. package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BteMjuOn.js → architectureDiagram-ZJ3FMSHR-CK19SQDx.js} +1 -1
  395. package/ui-dist/assets/{blockDiagram-677ZJIJ3-Iq66uZjf.js → blockDiagram-677ZJIJ3-Kh4N3XG4.js} +1 -1
  396. package/ui-dist/assets/{browser-ponyfill-BBsuyZJt.js → browser-ponyfill-B3hXtEFW.js} +1 -1
  397. package/ui-dist/assets/{c4Diagram-LMCZKHZV-_nlglj7I.js → c4Diagram-LMCZKHZV-CBHpGrgU.js} +1 -1
  398. package/ui-dist/assets/channel-4fjB8e8N.js +1 -0
  399. package/ui-dist/assets/{chunk-2Q5K7J3B-Cvz7WwSQ.js → chunk-2Q5K7J3B-BdHbqi7k.js} +1 -1
  400. package/ui-dist/assets/{chunk-32BRIVSS-LLubniCS.js → chunk-32BRIVSS-DxtzZzpt.js} +1 -1
  401. package/ui-dist/assets/{chunk-5VM5RSS4-BOst5hcr.js → chunk-5VM5RSS4-DMKxBbgu.js} +1 -1
  402. package/ui-dist/assets/{chunk-EX3LRPZG-D5WNShVm.js → chunk-EX3LRPZG-BaIOPi4v.js} +1 -1
  403. package/ui-dist/assets/{chunk-JWPE2WC7-CA07x4av.js → chunk-JWPE2WC7-DGo3MPmr.js} +1 -1
  404. package/ui-dist/assets/{chunk-MOJQB5TN-Clfi83rK.js → chunk-MOJQB5TN-BEhQg0uD.js} +1 -1
  405. package/ui-dist/assets/{chunk-RYQCIY6F-BhZ6p7YJ.js → chunk-RYQCIY6F-HPJ6zRpJ.js} +1 -1
  406. package/ui-dist/assets/{chunk-V7JOEXUC-CdexJGoV.js → chunk-V7JOEXUC-Bl_6cAJD.js} +1 -1
  407. package/ui-dist/assets/{chunk-VR4S4FIN-8vjmBhr1.js → chunk-VR4S4FIN-KMjgGCsw.js} +1 -1
  408. package/ui-dist/assets/{chunk-XXDRQBXY-CqmCaplj.js → chunk-XXDRQBXY-oWGAIKjr.js} +1 -1
  409. package/ui-dist/assets/classDiagram-OUVF2IWQ-CTXxO5l9.js +1 -0
  410. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CTXxO5l9.js +1 -0
  411. package/ui-dist/assets/{cose-bilkent-JH36ORCC-CY-zAygY.js → cose-bilkent-JH36ORCC-Ckqse1pH.js} +1 -1
  412. package/ui-dist/assets/{cynefin-VYW2F7L2-BQSQ1HKr.js → cynefin-VYW2F7L2-DfZ8Hzqi.js} +1 -1
  413. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-NJCuFrZL.js → cynefinDiagram-TSTJHNR4-BkDQo9oi.js} +1 -1
  414. package/ui-dist/assets/{dagre-VKFMJZFB-DAUPIc8g.js → dagre-VKFMJZFB-ClghXQ6E.js} +1 -1
  415. package/ui-dist/assets/{diagram-FQU43EPY-DvMwJlCi.js → diagram-FQU43EPY-D5a0CwpV.js} +1 -1
  416. package/ui-dist/assets/{diagram-G47NLZAW-DPY0Jz_y.js → diagram-G47NLZAW-hVjYR_xl.js} +1 -1
  417. package/ui-dist/assets/{diagram-NH7WQ7WH-BpopeEhp.js → diagram-NH7WQ7WH-BlDPgD5Z.js} +1 -1
  418. package/ui-dist/assets/{diagram-OA4YK3LP-Qpu05z9X.js → diagram-OA4YK3LP-D0iZKIS3.js} +1 -1
  419. package/ui-dist/assets/{diagram-WEI45ONY-DHnvw9XU.js → diagram-WEI45ONY-m6hrfD0J.js} +1 -1
  420. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-D61S54_7.js → ebnfDiagram-CCIWWBDH-D4SQYLod.js} +1 -1
  421. package/ui-dist/assets/{erDiagram-Q63AITRT-pplAN_x2.js → erDiagram-Q63AITRT-Byzb6-I5.js} +1 -1
  422. package/ui-dist/assets/{flowDiagram-23GEKE2U-BaBcbyDq.js → flowDiagram-23GEKE2U-CaTUkFO6.js} +1 -1
  423. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BOJSII2b.js → ganttDiagram-NO4QXBWP-DmdMQ3ES.js} +1 -1
  424. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-aYAaO4a2.js → gitGraphDiagram-IHSO6WYX-Bt-1g2CS.js} +1 -1
  425. package/ui-dist/assets/{index-CZxYajxF.js → index-2Dk9yO0W.js} +1 -1
  426. package/ui-dist/assets/{index-B8H0SHw1.js → index-8jLpgFNU.js} +1 -1
  427. package/ui-dist/assets/{index-3EKATXgc.js → index-AP_iimxs.js} +1 -1
  428. package/ui-dist/assets/{index-C2JgdjPa.js → index-BNJasQY9.js} +1 -1
  429. package/ui-dist/assets/{index-BaiskB3o.js → index-BO-aNmyX.js} +1 -1
  430. package/ui-dist/assets/{index-B5YenxAQ.js → index-BPCzCn1Y.js} +1 -1
  431. package/ui-dist/assets/{index-Da2kqT1M.js → index-BSLnLTb2.js} +1 -1
  432. package/ui-dist/assets/{index-By0oeEvz.js → index-CCZ2qgHN.js} +1 -1
  433. package/ui-dist/assets/{index-bUFCSEgv.js → index-CLZXR1k9.js} +1 -1
  434. package/ui-dist/assets/{index-CJlw4l9L.js → index-CTNfrE1s.js} +1 -1
  435. package/ui-dist/assets/{index-TmpY6Xh_.js → index-ChNd25s7.js} +1 -1
  436. package/ui-dist/assets/{index-BRgdHUNR.js → index-CjHiMOEO.js} +1 -1
  437. package/ui-dist/assets/{index-DrL6pHSJ.js → index-CmeJQpXZ.js} +1 -1
  438. package/ui-dist/assets/index-CyuYtn7U.css +1 -0
  439. package/ui-dist/assets/{index-CKKytnOd.js → index-CzZjM8eh.js} +1 -1
  440. package/ui-dist/assets/{index-Dceamza_.js → index-D-6T0Aft.js} +1 -1
  441. package/ui-dist/assets/{index-CjDqB1SX.js → index-DG_7V1pG.js} +1 -1
  442. package/ui-dist/assets/index-DIKcgxCt.js +676 -0
  443. package/ui-dist/assets/{index-BKVeJ3Jh.js → index-DiUerHmi.js} +1 -1
  444. package/ui-dist/assets/{index-C-nKjFZh.js → index-NZ7v5Fc2.js} +1 -1
  445. package/ui-dist/assets/{index-CvPPuNs1.js → index-Py70EvBg.js} +1 -1
  446. package/ui-dist/assets/{index-LHkZdu0h.js → index-R3aYn0qM.js} +1 -1
  447. package/ui-dist/assets/{index-BT8b79Bd.js → index-YXpm7gW-.js} +1 -1
  448. package/ui-dist/assets/{index-CemenVOf.js → index-n4q3gzki.js} +1 -1
  449. package/ui-dist/assets/{index-D3ODiUup.js → index-vlZ3GoYY.js} +1 -1
  450. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-C7_90qGq.js → infoDiagram-FWYZ7A6U-jdi5ciVg.js} +1 -1
  451. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-T-nPPdTt.js → ishikawaDiagram-FXEZZL3T-CMz5VvYp.js} +1 -1
  452. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-C3hqQN0R.js → journeyDiagram-5HDEW3XC-DK6dk2R-.js} +1 -1
  453. package/ui-dist/assets/{kanban-definition-HUTT4EX6-CPoD9DUm.js → kanban-definition-HUTT4EX6-DX5cZL-D.js} +1 -1
  454. package/ui-dist/assets/{linear-D-S8yr3Z.js → linear-CI96ABKp.js} +1 -1
  455. package/ui-dist/assets/{mermaid.core-DhvH8shK.js → mermaid.core-DnsEVfbd.js} +4 -4
  456. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-CxCIKrKo.js → mindmap-definition-LN4V7U3C-BdDCY7hf.js} +1 -1
  457. package/ui-dist/assets/{pegDiagram-2B236MQR-D4G-PC6N.js → pegDiagram-2B236MQR-DuY4TF-f.js} +1 -1
  458. package/ui-dist/assets/{pieDiagram-ENE6RG2P-N4X8UvqY.js → pieDiagram-ENE6RG2P-bgFw7dm3.js} +1 -1
  459. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CZohoDln.js → quadrantDiagram-ABIIQ3AL-BCZVWyWS.js} +1 -1
  460. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-fMGQVhwF.js → railroadDiagram-RFXS5EU6-D9V5_Cgc.js} +1 -1
  461. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-CK0IuUmk.js → requirementDiagram-TGXJPOKE-CItsya2r.js} +1 -1
  462. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-pSfb3CqG.js → sankeyDiagram-HTMAVEWB-Cz9IHsZO.js} +1 -1
  463. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CbhkqIeq.js → sequenceDiagram-DBY2YBRQ-hMSqTiwK.js} +1 -1
  464. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-B6vqqPx-.js → sizeCapture-X5ZJPWSS-0g9M5wMf.js} +1 -1
  465. package/ui-dist/assets/{stateDiagram-2N3HPSRC-DIJf9h5C.js → stateDiagram-2N3HPSRC-F8_YhgWl.js} +1 -1
  466. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-fZImUBtq.js +1 -0
  467. package/ui-dist/assets/{swimlanes-5IMT3BWC-DIX_LV8O.js → swimlanes-5IMT3BWC-CCZqPbpE.js} +2 -2
  468. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-9C1UT51R.js +8 -0
  469. package/ui-dist/assets/{timeline-definition-FHXFAJF6-CD-Pxb_g.js → timeline-definition-FHXFAJF6-BeUIr18x.js} +1 -1
  470. package/ui-dist/assets/{vennDiagram-L72KCM5P-BAZsWL0R.js → vennDiagram-L72KCM5P-D3d5h_G4.js} +1 -1
  471. package/ui-dist/assets/{wardleyDiagram-EHGQE667-CnNyfwX5.js → wardleyDiagram-EHGQE667-Bzt-As9A.js} +1 -1
  472. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-j9YgsD3Y.js → xychartDiagram-FW5EYKEG-C2mly0xf.js} +1 -1
  473. package/ui-dist/fonts/InterVariable-Italic.woff2 +0 -0
  474. package/ui-dist/fonts/InterVariable.woff2 +0 -0
  475. package/ui-dist/fonts/NOTICE.md +17 -0
  476. package/ui-dist/index.html +2 -2
  477. package/ui-dist/locales/en/common.json +1142 -5
  478. package/ui-dist/locales/zh-CN/common.json +1140 -3
  479. package/ui-dist/assets/channel-CsiI2LYw.js +0 -1
  480. package/ui-dist/assets/classDiagram-OUVF2IWQ-CBn1Jqo8.js +0 -1
  481. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CBn1Jqo8.js +0 -1
  482. package/ui-dist/assets/index-BsL8K1vL.css +0 -1
  483. package/ui-dist/assets/index-D__fTsb2.js +0 -643
  484. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DpwLD7YL.js +0 -1
  485. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-Cklauutg.js +0 -8
@@ -1,7 +1,8 @@
1
- import { and, desc, eq, isNull, sql } from "drizzle-orm";
2
- import { agents, companies, issues, projects } from "@penclipai/db";
3
- import { COMPANY_SEARCH_MAX_LIMIT, COMPANY_SEARCH_MAX_OFFSET, COMPANY_SEARCH_MAX_TOKENS, COMPANY_ARTIFACTS_MAX_LIMIT, COMPANY_ARTIFACTS_MAX_QUERY_LENGTH, } from "@penclipai/shared";
1
+ import { and, desc, eq, gte, inArray, isNotNull, isNull, notInArray, or, sql } from "drizzle-orm";
2
+ import { agents, assets, companies, documents, issueAttachments, issueDocuments, issueWorkProducts, issues, projects, } from "@penclipai/db";
3
+ import { COMPANY_SEARCH_MAX_LIMIT, COMPANY_SEARCH_MAX_OFFSET, COMPANY_SEARCH_MAX_TOKENS, COMPANY_SEARCH_UPDATED_WITHIN_OPTIONS, COMPANY_ARTIFACTS_MAX_LIMIT, COMPANY_ARTIFACTS_MAX_QUERY_LENGTH, ISSUE_PRIORITIES, ISSUE_STATUSES, SYSTEM_ISSUE_DOCUMENT_KEYS, } from "@penclipai/shared";
4
4
  import { companyArtifactsService } from "./company-artifacts.js";
5
+ import { visibleIssueCondition } from "./issue-visibility.js";
5
6
  const MIN_TOKEN_LENGTH = 2;
6
7
  const MIN_FUZZY_QUERY_LENGTH = 4;
7
8
  const MIN_FUZZY_TOKEN_LENGTH = 4;
@@ -43,14 +44,10 @@ function sqlTextArray(values) {
43
44
  return sql `ARRAY[]::text[]`;
44
45
  return sql `ARRAY[${sql.join(values.map((value) => sql `${value}`), sql `, `)}]::text[]`;
45
46
  }
46
- function tokenMatchExpression(textExpression, tokenArray) {
47
- return sql `
48
- EXISTS (
49
- SELECT 1
50
- FROM unnest(${tokenArray}) AS search_token(value)
51
- WHERE lower(coalesce(${textExpression}, '')) LIKE '%' || search_token.value || '%' ESCAPE '\\'
52
- )
53
- `;
47
+ function sqlUuidArray(values) {
48
+ if (values.length === 0)
49
+ return sql `ARRAY[]::uuid[]`;
50
+ return sql `ARRAY[${sql.join(values.map((value) => sql `${value}`), sql `, `)}]::uuid[]`;
54
51
  }
55
52
  function noMatchSql() {
56
53
  return sql `false`;
@@ -139,11 +136,211 @@ function issueHref(prefix, issue, suffix = "") {
139
136
  function matchTerms(normalizedQuery, tokens) {
140
137
  return [normalizedQuery, ...tokens].filter((term, index, terms) => term.length > 0 && terms.indexOf(term) === index);
141
138
  }
142
- function makeCounts(results) {
143
- const counts = { issue: 0, artifact: 0, agent: 0, project: 0 };
144
- for (const result of results)
145
- counts[result.type] += 1;
146
- return counts;
139
+ function emptySearchCounts() {
140
+ return { issue: 0, comment: 0, document: 0, artifact: 0, agent: 0, project: 0 };
141
+ }
142
+ function emptyFilterOptionCounts() {
143
+ return {
144
+ status: {},
145
+ priority: {},
146
+ assigneeAgentId: {},
147
+ assigneeUserId: {},
148
+ projectId: {},
149
+ labelId: {},
150
+ updatedWithin: {},
151
+ };
152
+ }
153
+ function priorityRank(priority) {
154
+ const index = ISSUE_PRIORITIES.indexOf(priority ?? "");
155
+ return index >= 0 ? index : ISSUE_PRIORITIES.length;
156
+ }
157
+ function updatedWithinStart(value, now = new Date()) {
158
+ if (!value)
159
+ return null;
160
+ const match = /^(\d+)(h|d|w|m)$/.exec(value);
161
+ if (!match)
162
+ return null;
163
+ const amount = Number.parseInt(match[1], 10);
164
+ const unit = match[2];
165
+ const hours = unit === "h" ? amount : unit === "d" ? amount * 24 : unit === "w" ? amount * 24 * 7 : amount * 24 * 30;
166
+ return new Date(now.getTime() - hours * 60 * 60 * 1000);
167
+ }
168
+ function issueOnlyFiltersActive(query) {
169
+ return query.status.length > 0
170
+ || query.priority.length > 0
171
+ || query.assigneeAgentId !== undefined
172
+ || Boolean(query.assigneeUserId)
173
+ || Boolean(query.projectId)
174
+ || Boolean(query.labelId)
175
+ || Boolean(query.updatedWithin)
176
+ || Boolean(query.updatedAfter);
177
+ }
178
+ function activeIssueFilters(query) {
179
+ const filters = [];
180
+ if (query.status.length > 0)
181
+ filters.push({ key: "status", values: query.status });
182
+ if (query.assigneeAgentId !== undefined)
183
+ filters.push({ key: "assigneeAgentId", values: [query.assigneeAgentId ?? "null"] });
184
+ if (query.assigneeUserId)
185
+ filters.push({ key: "assigneeUserId", values: [query.assigneeUserId] });
186
+ if (query.projectId)
187
+ filters.push({ key: "projectId", values: [query.projectId] });
188
+ if (query.labelId)
189
+ filters.push({ key: "labelId", values: [query.labelId] });
190
+ if (query.priority.length > 0)
191
+ filters.push({ key: "priority", values: query.priority });
192
+ if (query.updatedWithin)
193
+ filters.push({ key: "updatedWithin", values: [query.updatedWithin] });
194
+ if (query.updatedAfter)
195
+ filters.push({ key: "updatedAfter", values: [query.updatedAfter] });
196
+ return filters;
197
+ }
198
+ function queryWithoutFilter(query, key) {
199
+ return {
200
+ ...query,
201
+ status: key === "status" ? [] : query.status,
202
+ priority: key === "priority" ? [] : query.priority,
203
+ assigneeAgentId: key === "assigneeAgentId" ? undefined : query.assigneeAgentId,
204
+ assigneeUserId: key === "assigneeUserId" ? undefined : query.assigneeUserId,
205
+ projectId: key === "projectId" ? undefined : query.projectId,
206
+ labelId: key === "labelId" ? undefined : query.labelId,
207
+ updatedWithin: key === "updatedWithin" ? undefined : query.updatedWithin,
208
+ updatedAfter: key === "updatedAfter" ? undefined : query.updatedAfter,
209
+ };
210
+ }
211
+ function queryWithoutIssueFilters(query) {
212
+ return {
213
+ ...query,
214
+ status: [],
215
+ priority: [],
216
+ assigneeAgentId: undefined,
217
+ assigneeUserId: undefined,
218
+ projectId: undefined,
219
+ labelId: undefined,
220
+ updatedWithin: undefined,
221
+ updatedAfter: undefined,
222
+ };
223
+ }
224
+ function issueFilterConditions(companyId, query, omit) {
225
+ const conditions = [];
226
+ if (omit !== "status" && query.status.length > 0) {
227
+ conditions.push(query.status.length === 1 ? eq(issues.status, query.status[0]) : inArray(issues.status, query.status));
228
+ }
229
+ if (omit !== "priority" && query.priority.length > 0) {
230
+ conditions.push(query.priority.length === 1 ? eq(issues.priority, query.priority[0]) : inArray(issues.priority, query.priority));
231
+ }
232
+ if (omit !== "assigneeAgentId" && query.assigneeAgentId !== undefined) {
233
+ conditions.push(query.assigneeAgentId === null ? isNull(issues.assigneeAgentId) : eq(issues.assigneeAgentId, query.assigneeAgentId));
234
+ }
235
+ if (omit !== "assigneeUserId" && query.assigneeUserId) {
236
+ conditions.push(eq(issues.assigneeUserId, query.assigneeUserId));
237
+ }
238
+ if (omit !== "projectId" && query.projectId)
239
+ conditions.push(eq(issues.projectId, query.projectId));
240
+ if (omit !== "labelId" && query.labelId) {
241
+ conditions.push(sql `
242
+ EXISTS (
243
+ SELECT 1
244
+ FROM issue_labels search_filter_labels
245
+ WHERE search_filter_labels.company_id = ${companyId}
246
+ AND search_filter_labels.issue_id = ${issues.id}
247
+ AND search_filter_labels.label_id = ${query.labelId}
248
+ )
249
+ `);
250
+ }
251
+ if (omit !== "updatedWithin") {
252
+ const updatedWithin = updatedWithinStart(query.updatedWithin);
253
+ if (updatedWithin)
254
+ conditions.push(gte(issues.updatedAt, updatedWithin));
255
+ }
256
+ if (omit !== "updatedAfter" && query.updatedAfter) {
257
+ conditions.push(gte(issues.updatedAt, new Date(query.updatedAfter)));
258
+ }
259
+ return conditions;
260
+ }
261
+ // Facet conditions expressed against the `m` alias of the aggregate
262
+ // matched-issues CTE (plain columns, no drizzle table references).
263
+ function matchedFacetConditions(companyId, query, omit) {
264
+ const conditions = [];
265
+ if (omit !== "status" && query.status.length > 0) {
266
+ conditions.push(sql `m.status = ANY(${sqlTextArray(query.status)})`);
267
+ }
268
+ if (omit !== "priority" && query.priority.length > 0) {
269
+ conditions.push(sql `m.priority = ANY(${sqlTextArray(query.priority)})`);
270
+ }
271
+ if (omit !== "assigneeAgentId" && query.assigneeAgentId !== undefined) {
272
+ conditions.push(query.assigneeAgentId === null
273
+ ? sql `m.assignee_agent_id IS NULL`
274
+ : sql `m.assignee_agent_id = ${query.assigneeAgentId}`);
275
+ }
276
+ if (omit !== "assigneeUserId" && query.assigneeUserId) {
277
+ conditions.push(sql `m.assignee_user_id = ${query.assigneeUserId}`);
278
+ }
279
+ if (omit !== "projectId" && query.projectId) {
280
+ conditions.push(sql `m.project_id = ${query.projectId}`);
281
+ }
282
+ if (omit !== "labelId" && query.labelId) {
283
+ conditions.push(sql `
284
+ EXISTS (
285
+ SELECT 1
286
+ FROM issue_labels facet_filter_labels
287
+ WHERE facet_filter_labels.company_id = ${companyId}
288
+ AND facet_filter_labels.issue_id = m.id
289
+ AND facet_filter_labels.label_id = ${query.labelId}
290
+ )
291
+ `);
292
+ }
293
+ if (omit !== "updatedWithin") {
294
+ const updatedWithin = updatedWithinStart(query.updatedWithin);
295
+ // ISO strings: raw sql params bypass drizzle's column-level Date mapping.
296
+ if (updatedWithin)
297
+ conditions.push(sql `m.updated_at >= ${updatedWithin.toISOString()}::timestamptz`);
298
+ }
299
+ if (omit !== "updatedAfter" && query.updatedAfter) {
300
+ conditions.push(sql `m.updated_at >= ${new Date(query.updatedAfter).toISOString()}::timestamptz`);
301
+ }
302
+ return conditions;
303
+ }
304
+ function stripInternalSortFields(result) {
305
+ const { sortCreatedAt: _sortCreatedAt, sortPriorityRank: _sortPriorityRank, ...publicResult } = result;
306
+ return publicResult;
307
+ }
308
+ function compareSearchResults(sort) {
309
+ return (left, right) => {
310
+ if (sort === "updated") {
311
+ const updated = (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
312
+ if (updated !== 0)
313
+ return updated;
314
+ if (right.score !== left.score)
315
+ return right.score - left.score;
316
+ }
317
+ else if (sort === "created") {
318
+ const created = (right.sortCreatedAt ?? "").localeCompare(left.sortCreatedAt ?? "");
319
+ if (created !== 0)
320
+ return created;
321
+ const updated = (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
322
+ if (updated !== 0)
323
+ return updated;
324
+ }
325
+ else if (sort === "priority") {
326
+ const priority = left.sortPriorityRank - right.sortPriorityRank;
327
+ if (priority !== 0)
328
+ return priority;
329
+ const updated = (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
330
+ if (updated !== 0)
331
+ return updated;
332
+ if (right.score !== left.score)
333
+ return right.score - left.score;
334
+ }
335
+ else {
336
+ if (right.score !== left.score)
337
+ return right.score - left.score;
338
+ const updated = (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
339
+ if (updated !== 0)
340
+ return updated;
341
+ }
342
+ return right.id.localeCompare(left.id);
343
+ };
147
344
  }
148
345
  function scopeIncludesIssues(scope) {
149
346
  return scope === "all" || scope === "issues" || scope === "comments" || scope === "documents";
@@ -157,15 +354,6 @@ function scopeIncludesArtifacts(scope) {
157
354
  function scopeIncludesProjects(scope) {
158
355
  return scope === "all" || scope === "projects";
159
356
  }
160
- function issueSearchCondition(scope, input) {
161
- if (scope === "comments")
162
- return input.commentMatch;
163
- if (scope === "documents")
164
- return input.documentMatch;
165
- if (scope === "issues")
166
- return sql `(${input.issueTextMatch} OR ${input.fuzzyMatch})`;
167
- return sql `(${input.issueTextMatch} OR ${input.commentMatch} OR ${input.documentMatch} OR ${input.fuzzyMatch})`;
168
- }
169
357
  function selectPrimarySnippets(row, normalizedQuery, tokens) {
170
358
  const terms = matchTerms(normalizedQuery, tokens);
171
359
  const matchedFields = new Set(row.matchedFields ?? []);
@@ -253,6 +441,7 @@ function artifactResult(artifact, normalizedQuery, tokens) {
253
441
  description: [artifact.previewText, artifact.issue.identifier, artifact.issue.title, artifact.project?.name]
254
442
  .filter(Boolean)
255
443
  .join(" "),
444
+ createdAt: new Date(artifact.updatedAt),
256
445
  updatedAt: new Date(artifact.updatedAt),
257
446
  }, normalizedQuery, tokens);
258
447
  return {
@@ -270,9 +459,9 @@ function artifactResult(artifact, normalizedQuery, tokens) {
270
459
  previewImageUrl: artifact.mediaKind === "image" ? artifact.contentPath : null,
271
460
  };
272
461
  }
273
- function simpleTextCondition(fields, containsPattern, tokenArray) {
274
- const phraseConditions = fields.map((field) => sql `lower(coalesce(${field}, '')) LIKE ${containsPattern} ESCAPE '\\'`);
275
- const tokenConditions = fields.map((field) => tokenMatchExpression(field, tokenArray));
462
+ function simpleTextCondition(fields, containsPattern, tokenPatternArray) {
463
+ const phraseConditions = fields.map((field) => sql `coalesce(${field}, '') ILIKE ${containsPattern}`);
464
+ const tokenConditions = fields.map((field) => sql `coalesce(${field}, '') ILIKE ANY(${tokenPatternArray})`);
276
465
  return sql `(${sql.join([...phraseConditions, ...tokenConditions], sql ` OR `)})`;
277
466
  }
278
467
  export function companySearchBranchFetchLimit(limit, offset = 0) {
@@ -284,85 +473,111 @@ export function companySearchService(db) {
284
473
  return {
285
474
  search: async (companyId, query) => {
286
475
  const normalizedQuery = normalizeQuery(query.q);
476
+ const hasSearchText = normalizedQuery.length > 0;
287
477
  const tokens = tokenizeQuery(normalizedQuery);
288
478
  const scope = query.scope;
479
+ const sort = query.sort;
289
480
  const limit = query.limit;
290
481
  const offset = query.offset;
291
- const emptyCounts = { issue: 0, artifact: 0, agent: 0, project: 0 };
292
- if (normalizedQuery.length === 0) {
482
+ if (!hasSearchText && !issueOnlyFiltersActive(query)) {
293
483
  return {
294
484
  query: query.q,
295
485
  normalizedQuery,
296
486
  scope,
487
+ sort,
297
488
  limit,
298
489
  offset,
299
490
  results: [],
300
- countsByType: emptyCounts,
491
+ countsByType: emptySearchCounts(),
492
+ filterOptionCounts: emptyFilterOptionCounts(),
493
+ zeroResults: null,
301
494
  hasMore: false,
302
495
  };
303
496
  }
304
- const company = await db
305
- .select({ issuePrefix: companies.issuePrefix })
306
- .from(companies)
307
- .where(eq(companies.id, companyId))
308
- .then((rows) => rows[0] ?? null);
309
- const prefix = routePrefix(company?.issuePrefix);
310
497
  const fetchLimit = companySearchBranchFetchLimit(limit, offset);
311
498
  const escapedTokens = tokens.map(escapeLikePattern);
312
- const tokenArray = sqlTextArray(escapedTokens);
499
+ // LIKE/ILIKE both treat backslash as the default escape character, so the
500
+ // escaped tokens stay literal inside ILIKE ANY(...) patterns too.
501
+ const tokenPatterns = escapedTokens.map((token) => `%${token}%`);
502
+ const tokenPatternArray = sqlTextArray(tokenPatterns);
313
503
  const fuzzyTokens = fuzzyEligibleTokens(tokens);
314
504
  const fuzzyTokenArray = sqlTextArray(fuzzyTokens);
315
505
  const escapedQuery = escapeLikePattern(normalizedQuery);
316
- const containsPattern = `%${escapedQuery}%`;
317
- const startsWithPattern = `${escapedQuery}%`;
318
- const fuzzyEnabled = normalizedQuery.length >= MIN_FUZZY_QUERY_LENGTH && !/[\\%_]/.test(normalizedQuery);
506
+ const containsPattern = hasSearchText ? `%${escapedQuery}%` : "__paperclip_no_match__";
507
+ const startsWithPattern = hasSearchText ? `${escapedQuery}%` : "__paperclip_no_match__";
508
+ const fuzzyEnabled = hasSearchText && normalizedQuery.length >= MIN_FUZZY_QUERY_LENGTH && !/[\\%_]/.test(normalizedQuery);
319
509
  const fuzzyTokensEnabled = fuzzyEnabled && fuzzyTokens.length > 0;
320
- const titlePhraseMatch = sql `lower(${issues.title}) LIKE ${containsPattern} ESCAPE '\\'`;
321
- const titleStartsWith = sql `lower(${issues.title}) LIKE ${startsWithPattern} ESCAPE '\\'`;
322
- const identifierPhraseMatch = sql `lower(coalesce(${issues.identifier}, '')) LIKE ${containsPattern} ESCAPE '\\'`;
323
- const identifierStartsWith = sql `lower(coalesce(${issues.identifier}, '')) LIKE ${startsWithPattern} ESCAPE '\\'`;
324
- const descriptionPhraseMatch = sql `lower(coalesce(${issues.description}, '')) LIKE ${containsPattern} ESCAPE '\\'`;
325
- const titleTokenMatch = tokenMatchExpression(sql `${issues.title}`, tokenArray);
326
- const identifierTokenMatch = tokenMatchExpression(sql `${issues.identifier}`, tokenArray);
327
- const descriptionTokenMatch = tokenMatchExpression(sql `${issues.description}`, tokenArray);
328
- const issueTextMatch = sql `
329
- ${titlePhraseMatch}
330
- OR ${identifierPhraseMatch}
331
- OR ${descriptionPhraseMatch}
332
- OR ${titleTokenMatch}
333
- OR ${identifierTokenMatch}
334
- OR ${descriptionTokenMatch}
335
- `;
336
- const commentMatch = sql `
337
- EXISTS (
338
- SELECT 1
339
- FROM issue_comments search_comments
340
- WHERE search_comments.company_id = ${companyId}
341
- AND search_comments.issue_id = issues.id
342
- AND search_comments.deleted_at IS NULL
343
- AND (
344
- lower(search_comments.body) LIKE ${containsPattern} ESCAPE '\\'
345
- OR ${tokenMatchExpression(sql `search_comments.body`, tokenArray)}
346
- )
347
- )
348
- `;
349
- const documentMatch = sql `
350
- EXISTS (
351
- SELECT 1
352
- FROM issue_documents search_issue_documents
353
- INNER JOIN documents search_documents
354
- ON search_documents.id = search_issue_documents.document_id
355
- WHERE search_issue_documents.company_id = ${companyId}
356
- AND search_documents.company_id = ${companyId}
357
- AND search_issue_documents.issue_id = issues.id
358
- AND (
359
- lower(coalesce(search_documents.title, '')) LIKE ${containsPattern} ESCAPE '\\'
360
- OR lower(search_documents.latest_body) LIKE ${containsPattern} ESCAPE '\\'
361
- OR ${tokenMatchExpression(sql `search_documents.title`, tokenArray)}
362
- OR ${tokenMatchExpression(sql `search_documents.latest_body`, tokenArray)}
363
- )
364
- )
365
- `;
510
+ const tokenCount = tokens.length;
511
+ // --- shared match expressions against the `issues` table -------------
512
+ // Raw-column ILIKE keeps the predicates compatible with the existing
513
+ // pg_trgm GIN indexes (lower(col) LIKE expressions cannot use them).
514
+ const titlePhraseMatch = hasSearchText ? sql `issues.title ILIKE ${containsPattern}` : noMatchSql();
515
+ const titleStartsWith = hasSearchText ? sql `issues.title ILIKE ${startsWithPattern}` : noMatchSql();
516
+ const titleExactMatch = hasSearchText ? sql `lower(issues.title) = ${normalizedQuery}` : noMatchSql();
517
+ const identifierPhraseMatch = hasSearchText ? sql `coalesce(issues.identifier, '') ILIKE ${containsPattern}` : noMatchSql();
518
+ const identifierStartsWith = hasSearchText ? sql `coalesce(issues.identifier, '') ILIKE ${startsWithPattern}` : noMatchSql();
519
+ const identifierExactMatch = hasSearchText ? sql `lower(coalesce(issues.identifier, '')) = ${normalizedQuery}` : noMatchSql();
520
+ const descriptionPhraseMatch = hasSearchText ? sql `coalesce(issues.description, '') ILIKE ${containsPattern}` : noMatchSql();
521
+ const titleTokenMatch = tokenCount > 0 ? sql `issues.title ILIKE ANY(${tokenPatternArray})` : noMatchSql();
522
+ const identifierTokenMatch = tokenCount > 0 ? sql `coalesce(issues.identifier, '') ILIKE ANY(${tokenPatternArray})` : noMatchSql();
523
+ const descriptionTokenMatch = tokenCount > 0 ? sql `coalesce(issues.description, '') ILIKE ANY(${tokenPatternArray})` : noMatchSql();
524
+ // Comment/document matches are computed once per request into tagged
525
+ // CTEs (issue_id, ord) where ord 1 is the phrase pattern and ord k+1 is
526
+ // token k. Flags and per-token coverage become cheap hashed IN probes
527
+ // against those sets instead of per-issue-row correlated subqueries.
528
+ // Single-pattern queries stay a bare `col ILIKE pattern` so the pg_trgm
529
+ // GIN indexes can bitmap-scan them; multi-pattern queries use one tagged
530
+ // pass over the table (an OR/ANY form would seq-scan anyway).
531
+ const matchPatterns = hasSearchText
532
+ ? [containsPattern, ...tokenPatterns.filter((pattern) => pattern !== containsPattern)]
533
+ : [];
534
+ const matchPatternOrdinal = (pattern) => matchPatterns.indexOf(pattern) + 1;
535
+ const matchPatternArray = sqlTextArray(matchPatterns);
536
+ const commentMatchesCte = !hasSearchText
537
+ ? sql `SELECT NULL::uuid AS issue_id, 0 AS ord WHERE false`
538
+ : matchPatterns.length === 1
539
+ ? sql `
540
+ SELECT search_comments.issue_id, 1 AS ord
541
+ FROM issue_comments search_comments
542
+ WHERE search_comments.company_id = ${companyId}
543
+ AND search_comments.deleted_at IS NULL
544
+ AND search_comments.body ILIKE ${matchPatterns[0]}
545
+ GROUP BY 1, 2
546
+ `
547
+ : sql `
548
+ SELECT search_comments.issue_id, pat.ord::int AS ord
549
+ FROM issue_comments search_comments
550
+ INNER JOIN unnest(${matchPatternArray}) WITH ORDINALITY AS pat(pattern, ord)
551
+ ON search_comments.body ILIKE pat.pattern
552
+ WHERE search_comments.company_id = ${companyId}
553
+ AND search_comments.deleted_at IS NULL
554
+ GROUP BY 1, 2
555
+ `;
556
+ // Documents get one UNION ALL arm per pattern (each arm a bare
557
+ // `col ILIKE pattern`) so the planner can pick a pg_trgm bitmap scan per
558
+ // pattern; latest_body is large enough that skipping the seq scan for
559
+ // selective patterns dwarfs the duplicate-recheck cost on common ones.
560
+ const documentMatchesCte = !hasSearchText
561
+ ? sql `SELECT NULL::uuid AS issue_id, 0 AS ord WHERE false`
562
+ : sql.join(matchPatterns.map((pattern, index) => sql `
563
+ SELECT search_issue_documents.issue_id, ${index + 1}::int AS ord
564
+ FROM issue_documents search_issue_documents
565
+ INNER JOIN documents search_documents
566
+ ON search_documents.id = search_issue_documents.document_id
567
+ AND search_documents.company_id = search_issue_documents.company_id
568
+ WHERE search_issue_documents.company_id = ${companyId}
569
+ AND (
570
+ search_documents.title ILIKE ${pattern}
571
+ OR search_documents.latest_body ILIKE ${pattern}
572
+ )
573
+ GROUP BY 1, 2
574
+ `), sql ` UNION ALL `);
575
+ const commentMatch = hasSearchText
576
+ ? sql `issues.id IN (SELECT comment_matches.issue_id FROM comment_matches)`
577
+ : noMatchSql();
578
+ const documentMatch = hasSearchText
579
+ ? sql `issues.id IN (SELECT document_matches.issue_id FROM document_matches)`
580
+ : noMatchSql();
366
581
  // Each query token (length >= MIN_FUZZY_TOKEN_LENGTH) must have at least
367
582
  // one title word within Levenshtein edit distance. This handles typos
368
583
  // like "serach" -> "search" (transposition) and "mibile" -> "mobile"
@@ -382,7 +597,7 @@ export function companySearchService(db) {
382
597
  SELECT bool_and(
383
598
  EXISTS (
384
599
  SELECT 1
385
- FROM regexp_split_to_table(lower(${issues.title}), '[^a-z0-9]+') AS title_word(value)
600
+ FROM regexp_split_to_table(lower(issues.title), '[^a-z0-9]+') AS title_word(value)
386
601
  WHERE length(title_word.value) >= ${fuzzyMinTitleWordLengthExpr}
387
602
  AND levenshtein_less_equal(qt.value, title_word.value, ${fuzzyMaxEditsExpr}) <= ${fuzzyMaxEditsExpr}
388
603
  )
@@ -392,236 +607,542 @@ export function companySearchService(db) {
392
607
  `
393
608
  : noMatchSql();
394
609
  const fuzzyIdentifierMatch = fuzzyEnabled
395
- ? sql `similarity(lower(coalesce(${issues.identifier}, '')), ${normalizedQuery}) >= ${FUZZY_IDENTIFIER_SIMILARITY_THRESHOLD}`
610
+ ? sql `similarity(lower(coalesce(issues.identifier, '')), ${normalizedQuery}) >= ${FUZZY_IDENTIFIER_SIMILARITY_THRESHOLD}`
396
611
  : noMatchSql();
612
+ const issueTextMatch = sql `(
613
+ ${titlePhraseMatch}
614
+ OR ${identifierPhraseMatch}
615
+ OR ${descriptionPhraseMatch}
616
+ OR ${titleTokenMatch}
617
+ OR ${identifierTokenMatch}
618
+ OR ${descriptionTokenMatch}
619
+ )`;
397
620
  const fuzzyMatch = sql `(${fuzzyTokenTitleMatch} OR ${fuzzyIdentifierMatch})`;
398
- const tokenCoverage = sql `
399
- (
400
- SELECT count(*)::int
401
- FROM unnest(${tokenArray}) AS search_token(value)
402
- WHERE lower(${issues.title}) LIKE '%' || search_token.value || '%' ESCAPE '\\'
403
- OR lower(coalesce(${issues.identifier}, '')) LIKE '%' || search_token.value || '%' ESCAPE '\\'
404
- OR lower(coalesce(${issues.description}, '')) LIKE '%' || search_token.value || '%' ESCAPE '\\'
405
- OR EXISTS (
406
- SELECT 1
407
- FROM issue_comments coverage_comments
408
- WHERE coverage_comments.company_id = ${companyId}
409
- AND coverage_comments.issue_id = issues.id
410
- AND coverage_comments.deleted_at IS NULL
411
- AND lower(coverage_comments.body) LIKE '%' || search_token.value || '%' ESCAPE '\\'
412
- )
413
- OR EXISTS (
414
- SELECT 1
415
- FROM issue_documents coverage_issue_documents
416
- INNER JOIN documents coverage_documents
417
- ON coverage_documents.id = coverage_issue_documents.document_id
418
- WHERE coverage_issue_documents.company_id = ${companyId}
419
- AND coverage_documents.company_id = ${companyId}
420
- AND coverage_issue_documents.issue_id = issues.id
421
- AND (
422
- lower(coalesce(coverage_documents.title, '')) LIKE '%' || search_token.value || '%' ESCAPE '\\'
423
- OR lower(coverage_documents.latest_body) LIKE '%' || search_token.value || '%' ESCAPE '\\'
424
- )
425
- )
426
- )
427
- `;
428
- const tokenCount = tokens.length;
429
- const allTokensMatch = tokenCount > 0
430
- ? sql `${tokenCoverage} = ${tokenCount}`
431
- : noMatchSql();
432
- const score = sql `
433
- (
434
- CASE WHEN lower(coalesce(${issues.identifier}, '')) = ${normalizedQuery} THEN 1200 ELSE 0 END
435
- + CASE WHEN ${identifierStartsWith} THEN 700 ELSE 0 END
436
- + CASE WHEN lower(${issues.title}) = ${normalizedQuery} THEN 900 ELSE 0 END
437
- + CASE WHEN ${titleStartsWith} THEN 550 ELSE 0 END
438
- + CASE WHEN ${titlePhraseMatch} THEN 350 ELSE 0 END
439
- + CASE WHEN ${identifierPhraseMatch} THEN 320 ELSE 0 END
440
- + CASE WHEN ${commentMatch} THEN 180 ELSE 0 END
441
- + CASE WHEN ${documentMatch} THEN 170 ELSE 0 END
442
- + CASE WHEN ${descriptionPhraseMatch} THEN 120 ELSE 0 END
443
- + CASE WHEN ${allTokensMatch} THEN 260 ELSE 0 END
444
- + (${tokenCoverage} * 70)
445
- + CASE WHEN ${fuzzyMatch} THEN 110 ELSE 0 END
446
- + CASE ${issues.status} WHEN 'done' THEN 0 WHEN 'cancelled' THEN -30 ELSE 20 END
447
- )::double precision
448
- `;
449
- const matchedFields = sql `
450
- array_remove(ARRAY[
451
- CASE WHEN ${identifierPhraseMatch} OR ${identifierTokenMatch} OR ${fuzzyIdentifierMatch} THEN 'identifier' END,
452
- CASE WHEN ${titlePhraseMatch} OR ${titleTokenMatch} OR ${fuzzyTokenTitleMatch} THEN 'title' END,
453
- CASE WHEN ${descriptionPhraseMatch} OR ${descriptionTokenMatch} THEN 'description' END,
454
- CASE WHEN ${commentMatch} THEN 'comment' END,
455
- CASE WHEN ${documentMatch} THEN 'document' END
456
- ], NULL)::text[]
457
- `;
458
- const issueRows = scopeIncludesIssues(scope)
459
- ? await db
460
- .select({
461
- id: issues.id,
462
- identifier: issues.identifier,
463
- title: issues.title,
464
- description: issues.description,
465
- status: issues.status,
466
- priority: issues.priority,
467
- assigneeAgentId: issues.assigneeAgentId,
468
- assigneeUserId: issues.assigneeUserId,
469
- projectId: issues.projectId,
470
- updatedAt: issues.updatedAt,
471
- score,
472
- matchedFields,
473
- commentSnippet: sql `
474
- (
475
- SELECT search_comments.body
476
- FROM issue_comments search_comments
477
- WHERE search_comments.company_id = ${companyId}
478
- AND search_comments.issue_id = issues.id
479
- AND search_comments.deleted_at IS NULL
480
- AND (
481
- lower(search_comments.body) LIKE ${containsPattern} ESCAPE '\\'
482
- OR ${tokenMatchExpression(sql `search_comments.body`, tokenArray)}
483
- )
484
- ORDER BY
485
- CASE WHEN lower(search_comments.body) LIKE ${containsPattern} ESCAPE '\\' THEN 0 ELSE 1 END,
486
- search_comments.updated_at DESC,
487
- search_comments.id DESC
488
- LIMIT 1
489
- )
490
- `,
491
- commentId: sql `
492
- (
493
- SELECT search_comments.id
494
- FROM issue_comments search_comments
495
- WHERE search_comments.company_id = ${companyId}
496
- AND search_comments.issue_id = issues.id
497
- AND search_comments.deleted_at IS NULL
498
- AND (
499
- lower(search_comments.body) LIKE ${containsPattern} ESCAPE '\\'
500
- OR ${tokenMatchExpression(sql `search_comments.body`, tokenArray)}
501
- )
502
- ORDER BY
503
- CASE WHEN lower(search_comments.body) LIKE ${containsPattern} ESCAPE '\\' THEN 0 ELSE 1 END,
504
- search_comments.updated_at DESC,
505
- search_comments.id DESC
506
- LIMIT 1
507
- )
508
- `,
509
- documentSnippet: sql `
510
- (
511
- SELECT search_documents.latest_body
512
- FROM issue_documents search_issue_documents
513
- INNER JOIN documents search_documents
514
- ON search_documents.id = search_issue_documents.document_id
515
- WHERE search_issue_documents.company_id = ${companyId}
516
- AND search_documents.company_id = ${companyId}
517
- AND search_issue_documents.issue_id = issues.id
518
- AND (
519
- lower(coalesce(search_documents.title, '')) LIKE ${containsPattern} ESCAPE '\\'
520
- OR lower(search_documents.latest_body) LIKE ${containsPattern} ESCAPE '\\'
521
- OR ${tokenMatchExpression(sql `search_documents.title`, tokenArray)}
522
- OR ${tokenMatchExpression(sql `search_documents.latest_body`, tokenArray)}
523
- )
524
- ORDER BY
525
- CASE
526
- WHEN lower(coalesce(search_documents.title, '')) LIKE ${containsPattern} ESCAPE '\\' THEN 0
527
- WHEN lower(search_documents.latest_body) LIKE ${containsPattern} ESCAPE '\\' THEN 1
528
- ELSE 2
529
- END,
530
- search_documents.updated_at DESC,
531
- search_documents.id DESC
532
- LIMIT 1
533
- )
534
- `,
535
- documentTitle: sql `
536
- (
537
- SELECT search_documents.title
538
- FROM issue_documents search_issue_documents
539
- INNER JOIN documents search_documents
540
- ON search_documents.id = search_issue_documents.document_id
541
- WHERE search_issue_documents.company_id = ${companyId}
542
- AND search_documents.company_id = ${companyId}
543
- AND search_issue_documents.issue_id = issues.id
544
- AND (
545
- lower(coalesce(search_documents.title, '')) LIKE ${containsPattern} ESCAPE '\\'
546
- OR lower(search_documents.latest_body) LIKE ${containsPattern} ESCAPE '\\'
547
- OR ${tokenMatchExpression(sql `search_documents.title`, tokenArray)}
548
- OR ${tokenMatchExpression(sql `search_documents.latest_body`, tokenArray)}
549
- )
550
- ORDER BY search_documents.updated_at DESC, search_documents.id DESC
551
- LIMIT 1
621
+ const anySearchMatch = sql `(${issueTextMatch} OR ${commentMatch} OR ${documentMatch} OR ${fuzzyMatch})`;
622
+ const issueFilters = issueFilterConditions(companyId, query);
623
+ const hasIssueOnlyFilters = issueOnlyFiltersActive(query);
624
+ // Scope conditions over precomputed flag columns (alias-qualified).
625
+ function flagTextMatch(alias) {
626
+ return sql `(
627
+ ${sql.raw(alias)}.title_phrase OR ${sql.raw(alias)}.ident_phrase OR ${sql.raw(alias)}.desc_phrase
628
+ OR ${sql.raw(alias)}.title_token OR ${sql.raw(alias)}.ident_token OR ${sql.raw(alias)}.desc_token
629
+ )`;
630
+ }
631
+ function flagFuzzyMatch(alias) {
632
+ return sql `(${sql.raw(alias)}.fuzzy_title OR ${sql.raw(alias)}.fuzzy_ident)`;
633
+ }
634
+ function flagScopeCondition(alias, forScope) {
635
+ if (!hasSearchText) {
636
+ return forScope === "comments" || forScope === "documents" ? noMatchSql() : sql `true`;
637
+ }
638
+ if (forScope === "comments")
639
+ return sql `${sql.raw(alias)}.comment_match`;
640
+ if (forScope === "documents")
641
+ return sql `${sql.raw(alias)}.document_match`;
642
+ if (forScope === "issues")
643
+ return sql `(${flagTextMatch(alias)} OR ${flagFuzzyMatch(alias)})`;
644
+ return sql `(${flagTextMatch(alias)} OR ${sql.raw(alias)}.comment_match OR ${sql.raw(alias)}.document_match OR ${flagFuzzyMatch(alias)})`;
645
+ }
646
+ async function fetchIssueSearchData() {
647
+ const filtersActive = activeIssueFilters(query);
648
+ const scopeCond = flagScopeCondition("m", scope);
649
+ const optionCond = scopeIncludesIssues(scope) ? scopeCond : flagScopeCondition("m", "all");
650
+ const titleCond = hasSearchText ? sql `(${flagTextMatch("m")} OR ${flagFuzzyMatch("m")})` : sql `true`;
651
+ const facetsAll = matchedFacetConditions(companyId, query);
652
+ const branchWhere = (conditions) => conditions.length > 0 ? sql `WHERE ${sql.join(conditions, sql ` AND `)}` : sql ``;
653
+ // Count branches must match the result branch's column list; the
654
+ // trailing NULLs pad the issue data columns.
655
+ const countTail = sql `, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::text, NULL::text, NULL::uuid, NULL::text, NULL::uuid, NULL::timestamptz, NULL::timestamptz, NULL::double precision, NULL::text[]`;
656
+ const branches = [];
657
+ const wantResultRows = scopeIncludesIssues(scope)
658
+ && !(!hasSearchText && (scope === "comments" || scope === "documents"));
659
+ if (wantResultRows) {
660
+ const allTokensBonus = tokenCount > 0
661
+ ? sql `CASE WHEN m.token_coverage = ${tokenCount} THEN 260 ELSE 0 END`
662
+ : sql `0`;
663
+ const scoreSql = sql `(
664
+ CASE WHEN m.ident_exact THEN 1200 ELSE 0 END
665
+ + CASE WHEN m.ident_starts THEN 700 ELSE 0 END
666
+ + CASE WHEN m.title_exact THEN 900 ELSE 0 END
667
+ + CASE WHEN m.title_starts THEN 550 ELSE 0 END
668
+ + CASE WHEN m.title_phrase THEN 350 ELSE 0 END
669
+ + CASE WHEN m.ident_phrase THEN 320 ELSE 0 END
670
+ + CASE WHEN m.comment_match THEN 180 ELSE 0 END
671
+ + CASE WHEN m.document_match THEN 170 ELSE 0 END
672
+ + CASE WHEN m.desc_phrase THEN 120 ELSE 0 END
673
+ + ${allTokensBonus}
674
+ + (m.token_coverage * 70)
675
+ + CASE WHEN (m.fuzzy_title OR m.fuzzy_ident) THEN 110 ELSE 0 END
676
+ + CASE m.status WHEN 'done' THEN 0 WHEN 'cancelled' THEN -30 ELSE 20 END
677
+ )::double precision`;
678
+ const priorityOrderSql = sql `CASE m.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END`;
679
+ const orderBySql = sort === "updated"
680
+ ? sql `m.updated_at DESC, score DESC, m.id DESC`
681
+ : sort === "created"
682
+ ? sql `m.created_at DESC, m.updated_at DESC, m.id DESC`
683
+ : sort === "priority"
684
+ ? sql `${priorityOrderSql} ASC, m.updated_at DESC, score DESC, m.id DESC`
685
+ : sql `score DESC, m.updated_at DESC, m.id DESC`;
686
+ branches.push(sql `(
687
+ SELECT
688
+ 'result'::text AS kind,
689
+ NULL::text AS value,
690
+ 0 AS count,
691
+ m.id,
692
+ m.identifier,
693
+ m.title,
694
+ m.description,
695
+ m.status,
696
+ m.priority,
697
+ m.assignee_agent_id AS "assigneeAgentId",
698
+ m.assignee_user_id AS "assigneeUserId",
699
+ m.project_id AS "projectId",
700
+ m.created_at AS "createdAt",
701
+ m.updated_at AS "updatedAt",
702
+ ${scoreSql} AS score,
703
+ array_remove(ARRAY[
704
+ CASE WHEN m.ident_phrase OR m.ident_token OR m.fuzzy_ident THEN 'identifier' END,
705
+ CASE WHEN m.title_phrase OR m.title_token OR m.fuzzy_title THEN 'title' END,
706
+ CASE WHEN m.desc_phrase OR m.desc_token THEN 'description' END,
707
+ CASE WHEN m.comment_match THEN 'comment' END,
708
+ CASE WHEN m.document_match THEN 'document' END
709
+ ], NULL)::text[] AS "matchedFields"
710
+ FROM matched m
711
+ ${branchWhere([...facetsAll, scopeCond])}
712
+ ORDER BY ${orderBySql}
713
+ LIMIT ${fetchLimit}
714
+ )`);
715
+ }
716
+ if (scope === "all" || scope === "issues") {
717
+ branches.push(sql `SELECT 'type:issue' AS kind, NULL::text AS value, count(*)::int AS count ${countTail} FROM matched m ${branchWhere([...facetsAll, titleCond])}`);
718
+ }
719
+ if (hasSearchText && (scope === "all" || scope === "comments")) {
720
+ branches.push(sql `SELECT 'type:comment' AS kind, NULL::text AS value, count(*)::int AS count ${countTail} FROM matched m ${branchWhere([...facetsAll, sql `m.comment_match`])}`);
721
+ }
722
+ if (hasSearchText && (scope === "all" || scope === "documents")) {
723
+ branches.push(sql `SELECT 'type:document' AS kind, NULL::text AS value, count(*)::int AS count ${countTail} FROM matched m ${branchWhere([...facetsAll, sql `m.document_match`])}`);
724
+ }
725
+ const facetBranch = (kind, valueSql, omit, extra = []) => sql `
726
+ SELECT ${kind}::text AS kind, ${valueSql}::text AS value, count(*)::int AS count ${countTail}
727
+ FROM matched m
728
+ ${branchWhere([optionCond, ...matchedFacetConditions(companyId, query, omit), ...extra])}
729
+ GROUP BY 2
730
+ `;
731
+ branches.push(facetBranch("facet:status", sql `m.status`, "status"));
732
+ branches.push(facetBranch("facet:priority", sql `m.priority`, "priority"));
733
+ branches.push(facetBranch("facet:assigneeAgentId", sql `m.assignee_agent_id`, "assigneeAgentId", [sql `m.assignee_agent_id IS NOT NULL`]));
734
+ branches.push(facetBranch("facet:assigneeUserId", sql `m.assignee_user_id`, "assigneeUserId", [sql `m.assignee_user_id IS NOT NULL`]));
735
+ branches.push(facetBranch("facet:projectId", sql `m.project_id`, "projectId", [sql `m.project_id IS NOT NULL`]));
736
+ branches.push(sql `
737
+ SELECT 'facet:labelId' AS kind, matched_labels.label_id::text AS value, count(DISTINCT m.id)::int AS count ${countTail}
738
+ FROM matched m
739
+ INNER JOIN issue_labels matched_labels
740
+ ON matched_labels.issue_id = m.id
741
+ AND matched_labels.company_id = ${companyId}
742
+ ${branchWhere([optionCond, ...matchedFacetConditions(companyId, query, "labelId")])}
743
+ GROUP BY 2
744
+ `);
745
+ const updatedBaseQuery = { ...query, updatedWithin: undefined, updatedAfter: undefined };
746
+ const updatedBaseFacets = matchedFacetConditions(companyId, updatedBaseQuery);
747
+ for (const option of COMPANY_SEARCH_UPDATED_WITHIN_OPTIONS) {
748
+ const start = updatedWithinStart(option);
749
+ if (!start)
750
+ continue;
751
+ branches.push(sql `
752
+ SELECT 'facet:updatedWithin'::text AS kind, ${option}::text AS value, count(*)::int AS count ${countTail}
753
+ FROM matched m
754
+ ${branchWhere([optionCond, ...updatedBaseFacets, sql `m.updated_at >= ${start.toISOString()}::timestamptz`])}
755
+ `);
756
+ }
757
+ if (scopeIncludesIssues(scope)) {
758
+ branches.push(sql `SELECT 'total:current' AS kind, NULL::text AS value, count(*)::int AS count ${countTail} FROM matched m ${branchWhere([scopeCond, ...facetsAll])}`);
759
+ if (filtersActive.length > 0) {
760
+ branches.push(sql `SELECT 'total:unfiltered' AS kind, NULL::text AS value, count(*)::int AS count ${countTail} FROM matched m ${branchWhere([scopeCond])}`);
761
+ for (const filter of filtersActive) {
762
+ branches.push(sql `
763
+ SELECT ${`total:omit:${filter.key}`}::text AS kind, NULL::text AS value, count(*)::int AS count ${countTail}
764
+ FROM matched m
765
+ ${branchWhere([scopeCond, ...matchedFacetConditions(companyId, query, filter.key)])}
766
+ `);
767
+ }
768
+ }
769
+ }
770
+ // Per-token coverage counts matches across issue text and the tagged
771
+ // comment/document match sets (hashed IN probes, one set per token).
772
+ const coverageSql = tokenCount > 0
773
+ ? sql `(${sql.join(tokens.map((_, index) => {
774
+ const pattern = tokenPatterns[index];
775
+ const ord = matchPatternOrdinal(pattern);
776
+ return sql `(CASE WHEN
777
+ issues.title ILIKE ${pattern}
778
+ OR coalesce(issues.identifier, '') ILIKE ${pattern}
779
+ OR coalesce(issues.description, '') ILIKE ${pattern}
780
+ OR issues.id IN (SELECT comment_matches.issue_id FROM comment_matches WHERE comment_matches.ord = ${ord})
781
+ OR issues.id IN (SELECT document_matches.issue_id FROM document_matches WHERE document_matches.ord = ${ord})
782
+ THEN 1 ELSE 0 END)`;
783
+ }), sql ` + `)})`
784
+ : sql `0`;
785
+ const matchedWhere = hasSearchText ? sql ` AND ${anySearchMatch}` : sql ``;
786
+ const resultRows = await db.execute(sql `
787
+ WITH comment_matches AS MATERIALIZED (${commentMatchesCte}),
788
+ document_matches AS MATERIALIZED (${documentMatchesCte}),
789
+ matched AS MATERIALIZED (
790
+ SELECT
791
+ issues.id,
792
+ issues.identifier,
793
+ issues.title,
794
+ issues.description,
795
+ issues.status,
796
+ issues.priority,
797
+ issues.assignee_agent_id,
798
+ issues.assignee_user_id,
799
+ issues.project_id,
800
+ issues.created_at,
801
+ issues.updated_at,
802
+ ${titlePhraseMatch} AS title_phrase,
803
+ ${titleStartsWith} AS title_starts,
804
+ ${titleExactMatch} AS title_exact,
805
+ ${identifierPhraseMatch} AS ident_phrase,
806
+ ${identifierStartsWith} AS ident_starts,
807
+ ${identifierExactMatch} AS ident_exact,
808
+ ${descriptionPhraseMatch} AS desc_phrase,
809
+ ${titleTokenMatch} AS title_token,
810
+ ${identifierTokenMatch} AS ident_token,
811
+ ${descriptionTokenMatch} AS desc_token,
812
+ ${commentMatch} AS comment_match,
813
+ ${documentMatch} AS document_match,
814
+ ${fuzzyTokenTitleMatch} AS fuzzy_title,
815
+ ${fuzzyIdentifierMatch} AS fuzzy_ident,
816
+ ${coverageSql} AS token_coverage
817
+ FROM issues
818
+ WHERE issues.company_id = ${companyId}
819
+ AND ${visibleIssueCondition()}
820
+ ${matchedWhere}
821
+ )
822
+ ${sql.join(branches, sql ` UNION ALL `)}
823
+ `);
824
+ const aggregates = {
825
+ typeCounts: { issue: 0, comment: 0, document: 0 },
826
+ filterOptionCounts: emptyFilterOptionCounts(),
827
+ totals: { current: 0, unfiltered: 0, omit: {} },
828
+ };
829
+ const issueRowsRaw = [];
830
+ for (const row of resultRows) {
831
+ if (row.kind === "result") {
832
+ issueRowsRaw.push({
833
+ id: row.id,
834
+ identifier: row.identifier,
835
+ title: row.title,
836
+ description: row.description,
837
+ status: row.status,
838
+ priority: row.priority,
839
+ assigneeAgentId: row.assigneeAgentId,
840
+ assigneeUserId: row.assigneeUserId,
841
+ projectId: row.projectId,
842
+ createdAt: row.createdAt,
843
+ updatedAt: row.updatedAt,
844
+ score: row.score,
845
+ matchedFields: row.matchedFields,
846
+ commentSnippet: null,
847
+ commentId: null,
848
+ documentSnippet: null,
849
+ documentTitle: null,
850
+ documentKey: null,
851
+ });
852
+ continue;
853
+ }
854
+ const count = Number(row.count ?? 0);
855
+ if (row.kind === "type:issue")
856
+ aggregates.typeCounts.issue = count;
857
+ else if (row.kind === "type:comment")
858
+ aggregates.typeCounts.comment = count;
859
+ else if (row.kind === "type:document")
860
+ aggregates.typeCounts.document = count;
861
+ else if (row.kind === "facet:status" && row.value && ISSUE_STATUSES.includes(row.value)) {
862
+ aggregates.filterOptionCounts.status[row.value] = count;
863
+ }
864
+ else if (row.kind === "facet:priority" && row.value && ISSUE_PRIORITIES.includes(row.value)) {
865
+ aggregates.filterOptionCounts.priority[row.value] = count;
866
+ }
867
+ else if (row.kind === "facet:assigneeAgentId" && row.value)
868
+ aggregates.filterOptionCounts.assigneeAgentId[row.value] = count;
869
+ else if (row.kind === "facet:assigneeUserId" && row.value)
870
+ aggregates.filterOptionCounts.assigneeUserId[row.value] = count;
871
+ else if (row.kind === "facet:projectId" && row.value)
872
+ aggregates.filterOptionCounts.projectId[row.value] = count;
873
+ else if (row.kind === "facet:labelId" && row.value)
874
+ aggregates.filterOptionCounts.labelId[row.value] = count;
875
+ else if (row.kind === "facet:updatedWithin" && row.value) {
876
+ aggregates.filterOptionCounts.updatedWithin[row.value] = count;
877
+ }
878
+ else if (row.kind === "total:current")
879
+ aggregates.totals.current = count;
880
+ else if (row.kind === "total:unfiltered")
881
+ aggregates.totals.unfiltered = count;
882
+ else if (row.kind.startsWith("total:omit:")) {
883
+ aggregates.totals.omit[row.kind.slice("total:omit:".length)] = count;
884
+ }
885
+ }
886
+ return { rows: await enrichIssueSnippets(issueRowsRaw), aggregates };
887
+ }
888
+ // Fetch best-matching comment/document snippets only for the fetched
889
+ // page window (<= fetchLimit rows) instead of for every matching row.
890
+ async function enrichIssueSnippets(rows) {
891
+ if (!hasSearchText || rows.length === 0)
892
+ return rows;
893
+ const snippetIds = rows
894
+ .filter((row) => {
895
+ const fields = row.matchedFields ?? [];
896
+ return fields.includes("comment") || fields.includes("document");
897
+ })
898
+ .map((row) => row.id);
899
+ if (snippetIds.length === 0)
900
+ return rows;
901
+ const snippetRows = await db.execute(sql `
902
+ SELECT
903
+ target.id AS "issueId",
904
+ best_comment.id AS "commentId",
905
+ best_comment.body AS "commentSnippet",
906
+ best_document.latest_body AS "documentSnippet",
907
+ best_document.title AS "documentTitle",
908
+ best_document.key AS "documentKey"
909
+ FROM unnest(${sqlUuidArray(snippetIds)}) AS target(id)
910
+ LEFT JOIN LATERAL (
911
+ SELECT search_comments.id, search_comments.body
912
+ FROM issue_comments search_comments
913
+ WHERE search_comments.company_id = ${companyId}
914
+ AND search_comments.issue_id = target.id
915
+ AND search_comments.deleted_at IS NULL
916
+ AND (
917
+ search_comments.body ILIKE ${containsPattern}
918
+ OR search_comments.body ILIKE ANY(${tokenPatternArray})
552
919
  )
553
- `,
554
- documentKey: sql `
555
- (
556
- SELECT search_issue_documents.key
557
- FROM issue_documents search_issue_documents
558
- INNER JOIN documents search_documents
559
- ON search_documents.id = search_issue_documents.document_id
560
- WHERE search_issue_documents.company_id = ${companyId}
561
- AND search_documents.company_id = ${companyId}
562
- AND search_issue_documents.issue_id = issues.id
563
- AND (
564
- lower(coalesce(search_documents.title, '')) LIKE ${containsPattern} ESCAPE '\\'
565
- OR lower(search_documents.latest_body) LIKE ${containsPattern} ESCAPE '\\'
566
- OR ${tokenMatchExpression(sql `search_documents.title`, tokenArray)}
567
- OR ${tokenMatchExpression(sql `search_documents.latest_body`, tokenArray)}
568
- )
569
- ORDER BY search_documents.updated_at DESC, search_documents.id DESC
570
- LIMIT 1
920
+ ORDER BY
921
+ CASE WHEN search_comments.body ILIKE ${containsPattern} THEN 0 ELSE 1 END,
922
+ search_comments.updated_at DESC,
923
+ search_comments.id DESC
924
+ LIMIT 1
925
+ ) best_comment ON true
926
+ LEFT JOIN LATERAL (
927
+ SELECT search_issue_documents.key, search_documents.latest_body, search_documents.title
928
+ FROM issue_documents search_issue_documents
929
+ INNER JOIN documents search_documents
930
+ ON search_documents.id = search_issue_documents.document_id
931
+ AND search_documents.company_id = search_issue_documents.company_id
932
+ WHERE search_issue_documents.company_id = ${companyId}
933
+ AND search_issue_documents.issue_id = target.id
934
+ AND (
935
+ coalesce(search_documents.title, '') ILIKE ${containsPattern}
936
+ OR search_documents.latest_body ILIKE ${containsPattern}
937
+ OR coalesce(search_documents.title, '') ILIKE ANY(${tokenPatternArray})
938
+ OR search_documents.latest_body ILIKE ANY(${tokenPatternArray})
571
939
  )
572
- `,
573
- })
574
- .from(issues)
575
- .where(and(eq(issues.companyId, companyId), isNull(issues.hiddenAt), issueSearchCondition(scope, { issueTextMatch, commentMatch, documentMatch, fuzzyMatch })))
576
- .orderBy(desc(score), desc(issues.updatedAt), desc(issues.id))
577
- .limit(fetchLimit)
578
- : [];
940
+ ORDER BY
941
+ CASE
942
+ WHEN coalesce(search_documents.title, '') ILIKE ${containsPattern} THEN 0
943
+ WHEN search_documents.latest_body ILIKE ${containsPattern} THEN 1
944
+ ELSE 2
945
+ END,
946
+ search_documents.updated_at DESC,
947
+ search_documents.id DESC
948
+ LIMIT 1
949
+ ) best_document ON true
950
+ `);
951
+ const byIssueId = new Map(snippetRows.map((row) => [row.issueId, row]));
952
+ return rows.map((row) => {
953
+ const snippet = byIssueId.get(row.id);
954
+ if (!snippet)
955
+ return row;
956
+ return {
957
+ ...row,
958
+ commentSnippet: snippet.commentSnippet,
959
+ commentId: snippet.commentId,
960
+ documentSnippet: snippet.documentSnippet,
961
+ documentTitle: snippet.documentTitle,
962
+ documentKey: snippet.documentKey,
963
+ };
964
+ });
965
+ }
966
+ // --- agents / projects / artifacts ------------------------------------
579
967
  const simpleCondition = simpleTextCondition([
580
968
  sql `${agents.name}`,
581
969
  sql `${agents.role}`,
582
970
  sql `${agents.title}`,
583
971
  sql `${agents.capabilities}`,
584
- ], containsPattern, tokenArray);
585
- const agentRows = scopeIncludesAgents(scope)
586
- ? await db
972
+ ], containsPattern, tokenPatternArray);
973
+ const projectCondition = simpleTextCondition([
974
+ sql `${projects.name}`,
975
+ sql `${projects.description}`,
976
+ ], containsPattern, tokenPatternArray);
977
+ async function fetchAgentRows() {
978
+ if (!hasSearchText || !scopeIncludesAgents(scope) || hasIssueOnlyFilters)
979
+ return [];
980
+ return db
587
981
  .select({
588
982
  id: agents.id,
589
983
  title: agents.name,
590
984
  description: agents.capabilities,
591
985
  role: agents.role,
986
+ createdAt: agents.createdAt,
592
987
  updatedAt: agents.updatedAt,
593
988
  })
594
989
  .from(agents)
595
990
  .where(and(eq(agents.companyId, companyId), simpleCondition))
596
991
  .orderBy(desc(agents.updatedAt), desc(agents.id))
597
- .limit(fetchLimit)
598
- : [];
599
- const projectCondition = simpleTextCondition([
600
- sql `${projects.name}`,
601
- sql `${projects.description}`,
602
- ], containsPattern, tokenArray);
603
- const projectRows = scopeIncludesProjects(scope)
604
- ? await db
992
+ .limit(fetchLimit);
993
+ }
994
+ async function fetchProjectRows() {
995
+ if (!hasSearchText || !scopeIncludesProjects(scope) || hasIssueOnlyFilters)
996
+ return [];
997
+ return db
605
998
  .select({
606
999
  id: projects.id,
607
1000
  title: projects.name,
608
1001
  description: projects.description,
1002
+ createdAt: projects.createdAt,
609
1003
  updatedAt: projects.updatedAt,
610
1004
  })
611
1005
  .from(projects)
612
1006
  .where(and(eq(projects.companyId, companyId), isNull(projects.archivedAt), projectCondition))
613
1007
  .orderBy(desc(projects.updatedAt), desc(projects.id))
614
- .limit(fetchLimit)
615
- : [];
616
- const artifactRows = scopeIncludesArtifacts(scope)
617
- ? await companyArtifactsService(db).list(companyId, {
1008
+ .limit(fetchLimit);
1009
+ }
1010
+ async function countArtifacts(filters = query) {
1011
+ if (!hasSearchText)
1012
+ return 0;
1013
+ const artifactIssueFilters = issueFilterConditions(companyId, filters);
1014
+ const artifactIssueConditions = [
1015
+ eq(issues.companyId, companyId),
1016
+ visibleIssueCondition(),
1017
+ ...artifactIssueFilters,
1018
+ ];
1019
+ const documentArtifactConditions = [
1020
+ eq(issueDocuments.companyId, companyId),
1021
+ eq(documents.companyId, companyId),
1022
+ or(isNotNull(documents.createdByAgentId), isNotNull(documents.updatedByAgentId)),
1023
+ notInArray(issueDocuments.key, [...SYSTEM_ISSUE_DOCUMENT_KEYS]),
1024
+ sql `(
1025
+ coalesce(${documents.title}, '') ILIKE ${containsPattern} ESCAPE '\\'
1026
+ OR ${documents.latestBody} ILIKE ${containsPattern} ESCAPE '\\'
1027
+ OR coalesce(${issues.identifier}, '') ILIKE ${containsPattern} ESCAPE '\\'
1028
+ OR ${issues.title} ILIKE ${containsPattern} ESCAPE '\\'
1029
+ )`,
1030
+ ...artifactIssueConditions,
1031
+ ];
1032
+ const workProductConditions = [
1033
+ eq(issueWorkProducts.companyId, companyId),
1034
+ eq(issueWorkProducts.type, "artifact"),
1035
+ eq(issueWorkProducts.provider, "paperclip"),
1036
+ sql `(
1037
+ ${issueWorkProducts.title} ILIKE ${containsPattern} ESCAPE '\\'
1038
+ OR coalesce(${issueWorkProducts.summary}, '') ILIKE ${containsPattern} ESCAPE '\\'
1039
+ OR coalesce(${issues.identifier}, '') ILIKE ${containsPattern} ESCAPE '\\'
1040
+ OR ${issues.title} ILIKE ${containsPattern} ESCAPE '\\'
1041
+ )`,
1042
+ ...artifactIssueConditions,
1043
+ ];
1044
+ const attachmentConditions = [
1045
+ eq(issueAttachments.companyId, companyId),
1046
+ isNull(issueAttachments.issueCommentId),
1047
+ isNotNull(assets.createdByAgentId),
1048
+ sql `(
1049
+ coalesce(${assets.originalFilename}, '') ILIKE ${containsPattern} ESCAPE '\\'
1050
+ OR coalesce(${issues.identifier}, '') ILIKE ${containsPattern} ESCAPE '\\'
1051
+ OR ${issues.title} ILIKE ${containsPattern} ESCAPE '\\'
1052
+ )`,
1053
+ ...artifactIssueConditions,
1054
+ ];
1055
+ const [documentRows, workProductRows, attachmentRows] = await Promise.all([
1056
+ db
1057
+ .select({ count: sql `count(*)::int` })
1058
+ .from(issueDocuments)
1059
+ .innerJoin(documents, and(eq(issueDocuments.documentId, documents.id), eq(documents.companyId, issueDocuments.companyId)))
1060
+ .innerJoin(issues, and(eq(issueDocuments.issueId, issues.id), eq(issues.companyId, issueDocuments.companyId)))
1061
+ .where(and(...documentArtifactConditions)),
1062
+ db
1063
+ .select({ count: sql `count(*)::int` })
1064
+ .from(issueWorkProducts)
1065
+ .innerJoin(issues, and(eq(issueWorkProducts.issueId, issues.id), eq(issues.companyId, issueWorkProducts.companyId)))
1066
+ .where(and(...workProductConditions)),
1067
+ db
1068
+ .select({ count: sql `count(*)::int` })
1069
+ .from(issueAttachments)
1070
+ .innerJoin(assets, and(eq(issueAttachments.assetId, assets.id), eq(assets.companyId, issueAttachments.companyId)))
1071
+ .innerJoin(issues, and(eq(issueAttachments.issueId, issues.id), eq(issues.companyId, issueAttachments.companyId)))
1072
+ .where(and(...attachmentConditions)),
1073
+ ]);
1074
+ return Number(documentRows[0]?.count ?? 0)
1075
+ + Number(workProductRows[0]?.count ?? 0)
1076
+ + Number(attachmentRows[0]?.count ?? 0);
1077
+ }
1078
+ async function countAgents(filters = query) {
1079
+ if (!hasSearchText || issueOnlyFiltersActive(filters))
1080
+ return 0;
1081
+ const rows = await db
1082
+ .select({ count: sql `count(*)::int` })
1083
+ .from(agents)
1084
+ .where(and(eq(agents.companyId, companyId), simpleCondition));
1085
+ return Number(rows[0]?.count ?? 0);
1086
+ }
1087
+ async function countProjects(filters = query) {
1088
+ if (!hasSearchText || issueOnlyFiltersActive(filters))
1089
+ return 0;
1090
+ const rows = await db
1091
+ .select({ count: sql `count(*)::int` })
1092
+ .from(projects)
1093
+ .where(and(eq(projects.companyId, companyId), isNull(projects.archivedAt), projectCondition));
1094
+ return Number(rows[0]?.count ?? 0);
1095
+ }
1096
+ async function fetchArtifactRows() {
1097
+ if (!hasSearchText || !scopeIncludesArtifacts(scope))
1098
+ return [];
1099
+ const result = await companyArtifactsService(db).list(companyId, {
618
1100
  q: normalizedQuery.slice(0, COMPANY_ARTIFACTS_MAX_QUERY_LENGTH),
619
1101
  limit: Math.min(fetchLimit, COMPANY_ARTIFACTS_MAX_LIMIT),
620
- }).then((result) => result.artifacts)
621
- : [];
1102
+ }, { issueConditions: issueFilters });
1103
+ return result.artifacts;
1104
+ }
1105
+ const [company, issueSearchData, artifactRows, agentRows, projectRows, artifactCount, agentCount, projectCount] = await Promise.all([
1106
+ db
1107
+ .select({ issuePrefix: companies.issuePrefix })
1108
+ .from(companies)
1109
+ .where(eq(companies.id, companyId))
1110
+ .then((rows) => rows[0] ?? null),
1111
+ fetchIssueSearchData(),
1112
+ fetchArtifactRows(),
1113
+ fetchAgentRows(),
1114
+ fetchProjectRows(),
1115
+ scopeIncludesArtifacts(scope) ? countArtifacts(query) : Promise.resolve(0),
1116
+ scopeIncludesAgents(scope) ? countAgents(query) : Promise.resolve(0),
1117
+ scopeIncludesProjects(scope) ? countProjects(query) : Promise.resolve(0),
1118
+ ]);
1119
+ const prefix = routePrefix(company?.issuePrefix);
1120
+ const { rows: issueRows, aggregates } = issueSearchData;
1121
+ const countsByType = emptySearchCounts();
1122
+ countsByType.issue = aggregates.typeCounts.issue;
1123
+ countsByType.comment = aggregates.typeCounts.comment;
1124
+ countsByType.document = aggregates.typeCounts.document;
1125
+ countsByType.artifact = artifactCount;
1126
+ countsByType.agent = agentCount;
1127
+ countsByType.project = projectCount;
1128
+ const currentTotalCount = (scopeIncludesIssues(scope) ? aggregates.totals.current : 0)
1129
+ + artifactCount
1130
+ + agentCount
1131
+ + projectCount;
622
1132
  const results = [
623
- ...issueRows.map((row) => issueResult(row, prefix, normalizedQuery, tokens)),
624
- ...artifactRows.map((artifact) => artifactResult(artifact, normalizedQuery, tokens)),
1133
+ ...issueRows.map((row) => {
1134
+ const result = issueResult(row, prefix, normalizedQuery, tokens);
1135
+ return {
1136
+ ...result,
1137
+ sortCreatedAt: iso(row.createdAt),
1138
+ sortPriorityRank: priorityRank(row.priority),
1139
+ };
1140
+ }),
1141
+ ...artifactRows.map((artifact) => ({
1142
+ ...artifactResult(artifact, normalizedQuery, tokens),
1143
+ sortCreatedAt: artifact.updatedAt,
1144
+ sortPriorityRank: ISSUE_PRIORITIES.length,
1145
+ })),
625
1146
  ...agentRows.map((row) => {
626
1147
  const terms = matchTerms(normalizedQuery, tokens);
627
1148
  const snippet = createSnippet("capabilities", "Agent", row.description ?? row.role ?? row.title, terms);
@@ -637,6 +1158,8 @@ export function companySearchService(db) {
637
1158
  snippets: snippet ? [snippet] : [],
638
1159
  updatedAt: iso(row.updatedAt),
639
1160
  previewImageUrl: null,
1161
+ sortCreatedAt: iso(row.createdAt),
1162
+ sortPriorityRank: ISSUE_PRIORITIES.length,
640
1163
  };
641
1164
  }),
642
1165
  ...projectRows.map((row) => {
@@ -654,22 +1177,48 @@ export function companySearchService(db) {
654
1177
  snippets: snippet ? [snippet] : [],
655
1178
  updatedAt: iso(row.updatedAt),
656
1179
  previewImageUrl: null,
1180
+ sortCreatedAt: iso(row.createdAt),
1181
+ sortPriorityRank: ISSUE_PRIORITIES.length,
657
1182
  };
658
1183
  }),
659
- ].sort((left, right) => {
660
- if (right.score !== left.score)
661
- return right.score - left.score;
662
- return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
663
- });
664
- const paged = results.slice(offset, offset + limit);
1184
+ ].sort(compareSearchResults(sort));
1185
+ async function countTotalNonIssue(filters) {
1186
+ const [artifactTotal, agentTotal, projectTotal] = await Promise.all([
1187
+ scopeIncludesArtifacts(scope) ? countArtifacts(filters) : Promise.resolve(0),
1188
+ scopeIncludesAgents(scope) ? countAgents(filters) : Promise.resolve(0),
1189
+ scopeIncludesProjects(scope) ? countProjects(filters) : Promise.resolve(0),
1190
+ ]);
1191
+ return artifactTotal + agentTotal + projectTotal;
1192
+ }
1193
+ const filtersActive = activeIssueFilters(query);
1194
+ const zeroResults = currentTotalCount === 0 && filtersActive.length > 0
1195
+ ? {
1196
+ unfilteredTotal: (scopeIncludesIssues(scope) ? aggregates.totals.unfiltered : 0)
1197
+ + await countTotalNonIssue(queryWithoutIssueFilters(query)),
1198
+ loosenSuggestions: (await Promise.all(filtersActive.map(async (filter) => {
1199
+ const resultCount = (scopeIncludesIssues(scope) ? aggregates.totals.omit[filter.key] ?? 0 : 0)
1200
+ + await countTotalNonIssue(queryWithoutFilter(query, filter.key));
1201
+ return {
1202
+ filter: filter.key,
1203
+ values: filter.values,
1204
+ resultCount,
1205
+ additionalCount: Math.max(0, resultCount - currentTotalCount),
1206
+ };
1207
+ }))).sort((left, right) => right.additionalCount - left.additionalCount),
1208
+ }
1209
+ : null;
1210
+ const paged = results.slice(offset, offset + limit).map(stripInternalSortFields);
665
1211
  return {
666
1212
  query: query.q,
667
1213
  normalizedQuery,
668
1214
  scope,
1215
+ sort,
669
1216
  limit,
670
1217
  offset,
671
1218
  results: paged,
672
- countsByType: makeCounts(results),
1219
+ countsByType,
1220
+ filterOptionCounts: aggregates.filterOptionCounts,
1221
+ zeroResults,
673
1222
  hasMore: results.length > offset + limit,
674
1223
  };
675
1224
  },