collavre 0.24.0 → 0.25.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 (440) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -1
  3. data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
  4. data/app/assets/stylesheets/collavre/creatives.css +346 -37
  5. data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
  6. data/app/assets/stylesheets/collavre/gnb.css +64 -3
  7. data/app/assets/stylesheets/collavre/landing.css +108 -3
  8. data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
  9. data/app/assets/stylesheets/collavre/popup.css +14 -0
  10. data/app/assets/stylesheets/collavre/print.css +0 -1
  11. data/app/assets/stylesheets/collavre/search_popup.css +1 -1
  12. data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
  13. data/app/assets/stylesheets/collavre/workspace.css +307 -0
  14. data/app/channels/collavre/agent_channel.rb +14 -21
  15. data/app/channels/collavre/comments_presence_channel.rb +181 -21
  16. data/app/channels/collavre/inbox_badge_channel.rb +7 -1
  17. data/app/components/collavre/inbox/badge_component.rb +6 -13
  18. data/app/components/collavre/progress_filter_component.html.erb +2 -2
  19. data/app/components/collavre/progress_filter_component.rb +20 -0
  20. data/app/controllers/collavre/admin/settings_controller.rb +4 -1
  21. data/app/controllers/collavre/agent_connections_controller.rb +165 -0
  22. data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
  23. data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
  24. data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
  25. data/app/controllers/collavre/application_controller.rb +13 -2
  26. data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
  27. data/app/controllers/collavre/comments_controller.rb +45 -131
  28. data/app/controllers/collavre/concerns/shareable.rb +9 -3
  29. data/app/controllers/collavre/creatives_controller.rb +144 -11
  30. data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
  31. data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
  32. data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
  33. data/app/controllers/collavre/features_controller.rb +57 -0
  34. data/app/controllers/collavre/invites_controller.rb +12 -1
  35. data/app/controllers/collavre/llm_models_controller.rb +8 -0
  36. data/app/controllers/collavre/notices_controller.rb +20 -0
  37. data/app/controllers/collavre/tasks_controller.rb +40 -6
  38. data/app/controllers/collavre/topics_controller.rb +130 -53
  39. data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
  40. data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
  41. data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
  42. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
  43. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
  44. data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
  45. data/app/errors/collavre/turn_deadline_error.rb +20 -0
  46. data/app/helpers/collavre/application_helper.rb +38 -0
  47. data/app/helpers/collavre/creatives_helper.rb +56 -22
  48. data/app/helpers/collavre/navigation_helper.rb +12 -0
  49. data/app/javascript/collavre.js +1 -2
  50. data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
  51. data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
  52. data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
  53. data/app/javascript/components/creative_tree_row.js +62 -22
  54. data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
  55. data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
  56. data/app/javascript/controllers/__tests__/index.test.js +42 -0
  57. data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
  58. data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
  59. data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
  60. data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
  61. data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
  62. data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
  63. data/app/javascript/controllers/agent_connection_controller.js +357 -0
  64. data/app/javascript/controllers/agent_vendor_controller.js +23 -0
  65. data/app/javascript/controllers/comment_controller.js +3 -1
  66. data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
  67. data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
  68. data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
  69. data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
  70. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
  71. data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
  72. data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
  73. data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
  74. data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
  75. data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
  76. data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
  77. data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
  78. data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
  79. data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
  80. data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
  81. data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
  82. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
  83. data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
  84. data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
  85. data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
  86. data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
  87. data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
  88. data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
  89. data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
  90. data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
  91. data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
  92. data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
  93. data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
  94. data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
  95. data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
  96. data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
  97. data/app/javascript/controllers/comments/form_controller.js +795 -14
  98. data/app/javascript/controllers/comments/list_controller.js +338 -52
  99. data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
  100. data/app/javascript/controllers/comments/popup_controller.js +425 -83
  101. data/app/javascript/controllers/comments/presence_controller.js +317 -55
  102. data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
  103. data/app/javascript/controllers/comments/topics_controller.js +1772 -100
  104. data/app/javascript/controllers/common_popup_controller.js +2 -2
  105. data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
  106. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
  107. data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
  108. data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
  109. data/app/javascript/controllers/creatives/tree_controller.js +140 -31
  110. data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
  111. data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
  112. data/app/javascript/controllers/gateway_check_controller.js +36 -0
  113. data/app/javascript/controllers/index.js +25 -1
  114. data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
  115. data/app/javascript/controllers/link_creative_controller.js +89 -18
  116. data/app/javascript/controllers/progress_filter_controller.js +12 -22
  117. data/app/javascript/controllers/search_popup_controller.js +64 -46
  118. data/app/javascript/controllers/share_user_search_controller.js +2 -2
  119. data/app/javascript/controllers/topic_list_controller.js +44 -7
  120. data/app/javascript/controllers/workspace_tree_controller.js +662 -0
  121. data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
  122. data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
  123. data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
  124. data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
  125. data/app/javascript/creatives/tree_renderer.js +65 -22
  126. data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
  127. data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
  128. data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
  129. data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
  130. data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
  131. data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
  132. data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
  133. data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
  134. data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
  135. data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
  136. data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
  137. data/app/javascript/lib/api/creatives.js +32 -13
  138. data/app/javascript/lib/api/csrf_fetch.js +6 -4
  139. data/app/javascript/lib/api/topics.js +63 -5
  140. data/app/javascript/lib/chat_drafts.js +786 -0
  141. data/app/javascript/lib/common_popup.js +164 -25
  142. data/app/javascript/lib/creative_link_prefetch.js +27 -0
  143. data/app/javascript/lib/last_visited_creative.js +97 -0
  144. data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
  145. data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
  146. data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
  147. data/app/javascript/lib/lexical/creative_link_node.js +101 -0
  148. data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
  149. data/app/javascript/lib/lexical/link_toolbar.js +23 -0
  150. data/app/javascript/lib/turbo_stream_actions.js +23 -16
  151. data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
  152. data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
  153. data/app/javascript/lib/utils/filter_navigation.js +100 -0
  154. data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
  155. data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
  156. data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
  157. data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
  158. data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
  159. data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
  160. data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
  161. data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
  162. data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
  163. data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
  164. data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
  165. data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
  166. data/app/javascript/modules/command_args_form.js +0 -12
  167. data/app/javascript/modules/command_menu.js +80 -38
  168. data/app/javascript/modules/creative_link_picker.js +56 -0
  169. data/app/javascript/modules/creative_row_editor.js +446 -243
  170. data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
  171. data/app/javascript/modules/creative_tree_dom.js +4 -0
  172. data/app/javascript/modules/creative_tree_empty_state.js +80 -0
  173. data/app/javascript/modules/dom_listener_registry.js +23 -0
  174. data/app/javascript/modules/mention_menu.js +2 -3
  175. data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
  176. data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
  177. data/app/javascript/utils/caret_position.js +10 -0
  178. data/app/javascript/utils/chevron_icons.js +7 -0
  179. data/app/jobs/collavre/ai_agent_job.rb +262 -72
  180. data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
  181. data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
  182. data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
  183. data/app/jobs/collavre/comment_notification_job.rb +14 -0
  184. data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
  185. data/app/jobs/collavre/compress_job.rb +33 -22
  186. data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
  187. data/app/jobs/collavre/cron_action_job.rb +36 -18
  188. data/app/jobs/collavre/drop_trigger_job.rb +23 -9
  189. data/app/jobs/collavre/merge_comments_job.rb +33 -16
  190. data/app/jobs/collavre/push_notification_job.rb +62 -18
  191. data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
  192. data/app/jobs/collavre/stuck_detector_job.rb +1 -1
  193. data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
  194. data/app/models/collavre/agent_gateway.rb +200 -0
  195. data/app/models/collavre/agent_subscription.rb +4 -4
  196. data/app/models/collavre/agent_workspace.rb +210 -0
  197. data/app/models/collavre/comment/broadcastable.rb +22 -52
  198. data/app/models/collavre/comment/notifiable.rb +154 -77
  199. data/app/models/collavre/comment/topic_membership.rb +41 -0
  200. data/app/models/collavre/comment.rb +336 -31
  201. data/app/models/collavre/comment_notification_delivery.rb +61 -0
  202. data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
  203. data/app/models/collavre/comment_presence_store.rb +272 -12
  204. data/app/models/collavre/comment_read_pointer.rb +9 -15
  205. data/app/models/collavre/creative/describable.rb +8 -2
  206. data/app/models/collavre/creative/permissible.rb +11 -0
  207. data/app/models/collavre/creative.rb +29 -4
  208. data/app/models/collavre/creative_share.rb +109 -4
  209. data/app/models/collavre/current.rb +1 -0
  210. data/app/models/collavre/llm_model.rb +62 -0
  211. data/app/models/collavre/system_setting.rb +12 -1
  212. data/app/models/collavre/task.rb +121 -2
  213. data/app/models/collavre/topic.rb +51 -0
  214. data/app/models/collavre/user.rb +101 -41
  215. data/app/models/collavre/user_creative_preference.rb +3 -1
  216. data/app/services/collavre/agent_provisioning/archive.rb +108 -0
  217. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
  218. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
  219. data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
  220. data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
  221. data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
  222. data/app/services/collavre/ai_agent/message_builder.rb +103 -16
  223. data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
  224. data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
  225. data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
  226. data/app/services/collavre/ai_agent_service.rb +108 -20
  227. data/app/services/collavre/ai_client.rb +211 -21
  228. data/app/services/collavre/cli_proxy/client.rb +147 -0
  229. data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
  230. data/app/services/collavre/cli_proxy/identity.rb +47 -0
  231. data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
  232. data/app/services/collavre/command_menu_service.rb +0 -10
  233. data/app/services/collavre/comment_move_service.rb +96 -7
  234. data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
  235. data/app/services/collavre/comments/action_executor.rb +48 -20
  236. data/app/services/collavre/comments/command_processor.rb +0 -1
  237. data/app/services/collavre/comments/list_response.rb +133 -0
  238. data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
  239. data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
  240. data/app/services/collavre/comments/read_receipt_index.rb +152 -0
  241. data/app/services/collavre/comments/topic_command.rb +72 -1
  242. data/app/services/collavre/comments/topic_mutation.rb +20 -0
  243. data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
  244. data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
  245. data/app/services/collavre/creatives/reorderer.rb +15 -3
  246. data/app/services/collavre/creatives/tree_builder.rb +1 -1
  247. data/app/services/collavre/creatives/tree_formatter.rb +5 -6
  248. data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
  249. data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
  250. data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
  251. data/app/services/collavre/home_page_path.rb +55 -0
  252. data/app/services/collavre/html_text.rb +64 -0
  253. data/app/services/collavre/http_client.rb +11 -2
  254. data/app/services/collavre/inbox_reply_service.rb +1 -1
  255. data/app/services/collavre/markdown_converter.rb +13 -1
  256. data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
  257. data/app/services/collavre/orchestration/arbiter.rb +48 -5
  258. data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
  259. data/app/services/collavre/orchestration/delivery_record.rb +792 -0
  260. data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
  261. data/app/services/collavre/orchestration/matcher.rb +132 -17
  262. data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
  263. data/app/services/collavre/orchestration/scheduler.rb +10 -4
  264. data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
  265. data/app/services/collavre/orchestration/selection.rb +43 -0
  266. data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
  267. data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
  268. data/app/services/collavre/orchestration/topic_slot.rb +73 -0
  269. data/app/services/collavre/system_events/context_builder.rb +60 -11
  270. data/app/services/collavre/system_events/dispatcher.rb +58 -12
  271. data/app/services/collavre/system_events/envelope.rb +73 -0
  272. data/app/services/collavre/system_events/vocabulary.rb +45 -0
  273. data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
  274. data/app/services/collavre/tools/cron_create_service.rb +32 -18
  275. data/app/services/collavre/tools/id_list.rb +38 -0
  276. data/app/services/collavre/tools/topic_authorizer.rb +61 -7
  277. data/app/services/collavre/tools/topic_branch_service.rb +85 -0
  278. data/app/services/collavre/tools/topic_create_service.rb +117 -0
  279. data/app/services/collavre/tools/topic_list_service.rb +101 -0
  280. data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
  281. data/app/services/collavre/tools/topic_messages_service.rb +346 -0
  282. data/app/services/collavre/tools/topic_move_service.rb +108 -0
  283. data/app/services/collavre/tools/topic_selection.rb +59 -0
  284. data/app/services/collavre/tools/topic_update_service.rb +165 -0
  285. data/app/services/collavre/topic_branch_service.rb +39 -10
  286. data/app/services/collavre/topics/agent_resolver.rb +87 -0
  287. data/app/services/collavre/topics/char_budget.rb +70 -0
  288. data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
  289. data/app/services/collavre/topics/message_page.rb +380 -0
  290. data/app/services/collavre/topics/message_page_markdown.rb +102 -0
  291. data/app/services/collavre/topics/message_scope.rb +61 -0
  292. data/app/services/collavre/topics/message_stats.rb +92 -0
  293. data/app/services/collavre/topics/move_blocker.rb +47 -0
  294. data/app/services/collavre/topics/next_name.rb +35 -0
  295. data/app/services/collavre/topics/pin_rejection.rb +25 -0
  296. data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
  297. data/app/services/collavre/topics/reserved_name.rb +18 -0
  298. data/app/services/collavre/topics/serializer.rb +92 -0
  299. data/app/services/collavre/topics/topic_destroy.rb +30 -0
  300. data/app/services/collavre/topics/topic_move.rb +80 -0
  301. data/app/services/collavre/topics/topic_move_effects.rb +35 -0
  302. data/app/services/collavre/topics/topic_mutation.rb +27 -0
  303. data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
  304. data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
  305. data/app/views/collavre/agent_connections/show.html.erb +77 -0
  306. data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
  307. data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
  308. data/app/views/collavre/agent_gateways/index.html.erb +54 -0
  309. data/app/views/collavre/agent_gateways/new.html.erb +2 -0
  310. data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
  311. data/app/views/collavre/comments/_empty_state.html.erb +33 -0
  312. data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
  313. data/app/views/collavre/comments/_feature_card.html.erb +35 -0
  314. data/app/views/collavre/comments/_list.html.erb +3 -2
  315. data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
  316. data/app/views/collavre/creatives/_add_button.html.erb +17 -19
  317. data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
  318. data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
  319. data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
  320. data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
  321. data/app/views/collavre/creatives/index.html.erb +79 -18
  322. data/app/views/collavre/desktop_setup/show.html.erb +114 -0
  323. data/app/views/collavre/features/index.html.erb +42 -0
  324. data/app/views/collavre/features/show.html.erb +52 -0
  325. data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
  326. data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
  327. data/app/views/collavre/invites/show.html.erb +14 -1
  328. data/app/views/collavre/landing/show.html.erb +5 -1
  329. data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
  330. data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
  331. data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
  332. data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
  333. data/app/views/collavre/user_themes/index.html.erb +1 -1
  334. data/app/views/collavre/users/_llm_fields.html.erb +65 -0
  335. data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
  336. data/app/views/collavre/users/edit_ai.html.erb +33 -31
  337. data/app/views/collavre/users/new.html.erb +4 -2
  338. data/app/views/collavre/users/new_ai.html.erb +14 -32
  339. data/app/views/collavre/users/show.html.erb +13 -1
  340. data/app/views/layouts/collavre/landing.html.erb +9 -4
  341. data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
  342. data/config/locales/admin.en.yml +3 -1
  343. data/config/locales/admin.ko.yml +3 -1
  344. data/config/locales/agent_gateway.en.yml +217 -0
  345. data/config/locales/agent_gateway.ko.yml +217 -0
  346. data/config/locales/ai_agent.en.yml +0 -1
  347. data/config/locales/ai_agent.ko.yml +0 -1
  348. data/config/locales/comments.en.yml +72 -30
  349. data/config/locales/comments.ko.yml +72 -30
  350. data/config/locales/creatives.en.yml +32 -1
  351. data/config/locales/creatives.ko.yml +32 -1
  352. data/config/locales/features.en.yml +140 -0
  353. data/config/locales/features.ko.yml +140 -0
  354. data/config/locales/invites.en.yml +3 -0
  355. data/config/locales/invites.ko.yml +3 -0
  356. data/config/locales/landing.en.yml +2 -1
  357. data/config/locales/landing.ko.yml +2 -1
  358. data/config/locales/tools.en.yml +20 -0
  359. data/config/locales/tools.ko.yml +20 -0
  360. data/config/locales/users.en.yml +15 -14
  361. data/config/locales/users.ko.yml +14 -14
  362. data/config/routes.rb +45 -19
  363. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
  364. data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
  365. data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
  366. data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
  367. data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
  368. data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
  369. data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
  370. data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
  371. data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
  372. data/db/migrate/20260809000001_create_llm_models.rb +64 -0
  373. data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
  374. data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
  375. data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
  376. data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
  377. data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
  378. data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
  379. data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
  380. data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
  381. data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
  382. data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
  383. data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
  384. data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
  385. data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
  386. data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
  387. data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
  388. data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
  389. data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
  390. data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
  391. data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
  392. data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
  393. data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
  394. data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
  395. data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
  396. data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
  397. data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
  398. data/db/seeds.rb +1 -51
  399. data/lib/collavre/engine.rb +108 -1
  400. data/lib/collavre/feature_card_registry.rb +168 -0
  401. data/lib/collavre/hashed_access_token_lookup.rb +22 -0
  402. data/lib/collavre/sensitive_request_silencer.rb +23 -0
  403. data/lib/collavre/user_extensions.rb +0 -2
  404. data/lib/collavre/version.rb +1 -1
  405. data/lib/collavre.rb +1 -0
  406. data/skills/collavre/SKILL.md +165 -0
  407. data/skills/collavre/package.json +8 -0
  408. data/skills/collavre/references/tool-reference.md +347 -0
  409. data/skills/collavre/scripts/collavre +634 -0
  410. data/skills/collavre/scripts/html_entities.js +24 -0
  411. metadata +228 -30
  412. data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
  413. data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
  414. data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
  415. data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
  416. data/app/controllers/collavre/inbox_items_controller.rb +0 -70
  417. data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
  418. data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
  419. data/app/javascript/lib/typo_correction.js +0 -146
  420. data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
  421. data/app/javascript/modules/creative_guide.js +0 -67
  422. data/app/javascript/modules/typo_corrector.js +0 -534
  423. data/app/jobs/collavre/inbox_summary_job.rb +0 -24
  424. data/app/mailers/collavre/inbox_mailer.rb +0 -19
  425. data/app/models/collavre/inbox_item.rb +0 -89
  426. data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
  427. data/app/services/collavre/comments/work_command.rb +0 -161
  428. data/app/services/collavre/comments/workflow_executor.rb +0 -276
  429. data/app/services/collavre/gemini_parent_recommender.rb +0 -77
  430. data/app/services/collavre/mobile/event_summarizer.rb +0 -40
  431. data/app/services/collavre/typo_corrector.rb +0 -188
  432. data/app/views/collavre/inbox_items/_item.html.erb +0 -18
  433. data/app/views/collavre/inbox_items/_items.html.erb +0 -3
  434. data/app/views/collavre/inbox_items/_list.html.erb +0 -7
  435. data/app/views/collavre/inbox_items/index.html.erb +0 -1
  436. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
  437. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
  438. data/app/views/collavre/users/typo_correction.html.erb +0 -50
  439. data/config/locales/mobile.en.yml +0 -16
  440. data/config/locales/mobile.ko.yml +0 -16
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The CLI proxy now carries two identity axes: X-CLI-Proxy-User-ID selects the
4
+ # OS worker holding the engine credentials, and X-CLI-Proxy-Workspace-ID selects
5
+ # a path workspace below that worker's HOME. Split the single proxy_user_id into
6
+ # those axes so engine login is per Collavre user while skills and workspace
7
+ # config stay per agent.
8
+ #
9
+ # Expand only. bin/docker-entrypoint runs db:migrate while the previous release
10
+ # is still serving requests against the same database, so proxy_user_id has to
11
+ # stay readable and writable until every process runs the new code. The new
12
+ # columns are therefore nullable here and the old one merely loses its NOT NULL,
13
+ # which also keeps a Kamal rollback working. The contract half — dropping
14
+ # proxy_user_id and adding NOT NULL to the two new columns — belongs to a later
15
+ # release, once no process writes the legacy column anymore.
16
+ class SplitAgentWorkspaceIdentityAxes < ActiveRecord::Migration[8.0]
17
+ class MigrationWorkspace < ActiveRecord::Base
18
+ self.table_name = "agent_workspaces"
19
+ end
20
+
21
+ def up
22
+ add_column :agent_workspaces, :proxy_credential_id, :string
23
+ add_column :agent_workspaces, :proxy_workspace_id, :string
24
+
25
+ MigrationWorkspace.reset_column_information
26
+ MigrationWorkspace.find_each do |workspace|
27
+ workspace.update_columns(
28
+ proxy_workspace_id: "agent-#{workspace.agent_id}",
29
+ proxy_credential_id: credential_id_for(workspace)
30
+ )
31
+ end
32
+
33
+ change_column_null :agent_workspaces, :proxy_user_id, true
34
+ end
35
+
36
+ def down
37
+ MigrationWorkspace.reset_column_information
38
+ MigrationWorkspace.where(proxy_user_id: nil).find_each do |workspace|
39
+ workspace.update_columns(proxy_user_id: legacy_proxy_user_id_for(workspace))
40
+ end
41
+
42
+ change_column_null :agent_workspaces, :proxy_user_id, false
43
+ remove_column :agent_workspaces, :proxy_workspace_id
44
+ remove_column :agent_workspaces, :proxy_credential_id
45
+ end
46
+
47
+ private
48
+
49
+ def credential_id_for(workspace)
50
+ workspace.user_id ? "user-#{workspace.user_id}" : "agent-#{workspace.agent_id}"
51
+ end
52
+
53
+ def legacy_proxy_user_id_for(workspace)
54
+ return "agent-#{workspace.agent_id}" unless workspace.user_id
55
+
56
+ "agent-#{workspace.agent_id}--user-#{workspace.user_id}"
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ class SeedCodexCustomProxyLlmModels < ActiveRecord::Migration[8.1]
2
+ MAX_SUGGESTIONS = 100
3
+ DEFAULT_MODELS = [
4
+ "paperclip/codex_custom/anthropic/claude-sonnet-4.5",
5
+ "paperclip/codex_custom/openai/gpt-5"
6
+ ].freeze
7
+
8
+ def up
9
+ DEFAULT_MODELS.each do |name|
10
+ model = llm_model_class.find_or_create_by!(llm_vendor: "cli_proxy", name:)
11
+ model.touch unless model.previously_new_record?
12
+ end
13
+
14
+ keep_ids = llm_model_class.order(updated_at: :desc, id: :desc).limit(MAX_SUGGESTIONS).select(:id)
15
+ llm_model_class.where.not(id: keep_ids).delete_all
16
+ end
17
+
18
+ # Keep shared, user-created model suggestions intact if this data migration is rolled back.
19
+ def down; end
20
+
21
+ private
22
+
23
+ def llm_model_class
24
+ @llm_model_class ||= Class.new(ActiveRecord::Base) do
25
+ self.table_name = "llm_models"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,8 @@
1
+ class AddLastVisitedCreativeToUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :users, :last_visited_creative, foreign_key: {
4
+ to_table: :creatives,
5
+ on_delete: :nullify
6
+ }
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ class AddLastVisitedCreativeAtToUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :users, :last_visited_creative_at, :datetime
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddLastVisitedCreativeNavigationOrderToUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :users, :last_visited_creative_client_id, :string
4
+ add_column :users, :last_visited_creative_visit_sequence, :bigint
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddLastVisitedCreativeIssuedSequenceToUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :users, :last_visited_creative_issued_sequence, :bigint
4
+ end
5
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDesktopManagedToAgentGateways < ActiveRecord::Migration[8.1]
4
+ def up
5
+ add_column :agent_gateways, :desktop_managed, :boolean, default: false, null: false unless column_exists?(:agent_gateways, :desktop_managed)
6
+ backfill_legacy_desktop_gateway
7
+ add_index :agent_gateways, :desktop_managed, unique: true, where: "desktop_managed", name: "index_agent_gateways_on_desktop_managed" unless index_exists?(:agent_gateways, name: "index_agent_gateways_on_desktop_managed")
8
+ end
9
+
10
+ def down
11
+ remove_index :agent_gateways, name: "index_agent_gateways_on_desktop_managed" if index_exists?(:agent_gateways, name: "index_agent_gateways_on_desktop_managed")
12
+ remove_column :agent_gateways, :desktop_managed if column_exists?(:agent_gateways, :desktop_managed)
13
+ end
14
+
15
+ private
16
+
17
+ def backfill_legacy_desktop_gateway
18
+ # Legacy records have no dedicated marker. A generated CLI preset linked
19
+ # to the gateway is the durable signature; tenant ID and display fields
20
+ # alone remain user-editable and must not cause a gateway to be adopted.
21
+ legacy_gateway_ids = select_values(<<~SQL.squish)
22
+ SELECT id FROM agent_gateways
23
+ WHERE desktop_managed = FALSE
24
+ AND base_url LIKE 'http://127.0.0.1:%'
25
+ AND EXISTS (
26
+ SELECT 1 FROM users
27
+ WHERE users.agent_gateway_id = agent_gateways.id
28
+ AND users.created_by_id = agent_gateways.owner_id
29
+ AND users.email IN (
30
+ 'collavre-desktop-claude-code@ai.local',
31
+ 'collavre-desktop-codex@ai.local'
32
+ )
33
+ AND users.llm_vendor = 'cli_proxy'
34
+ )
35
+ SQL
36
+ return unless legacy_gateway_ids.one?
37
+
38
+ execute "UPDATE agent_gateways SET desktop_managed = TRUE WHERE id = #{connection.quote(legacy_gateway_ids.first)}"
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDesktopPresetAdapterToUsers < ActiveRecord::Migration[8.1]
4
+ PRESET_EMAILS = {
5
+ "claude" => "collavre-desktop-claude-code@ai.local",
6
+ "codex" => "collavre-desktop-codex@ai.local"
7
+ }.freeze
8
+
9
+ def up
10
+ add_column :users, :desktop_preset_adapter, :string unless column_exists?(:users, :desktop_preset_adapter)
11
+ backfill_legacy_desktop_presets
12
+ add_index :users, :desktop_preset_adapter, unique: true,
13
+ where: "desktop_preset_adapter IS NOT NULL",
14
+ name: "index_users_on_desktop_preset_adapter" unless index_exists?(:users, name: "index_users_on_desktop_preset_adapter")
15
+ end
16
+
17
+ def down
18
+ remove_index :users, name: "index_users_on_desktop_preset_adapter" if index_exists?(:users, name: "index_users_on_desktop_preset_adapter")
19
+ remove_column :users, :desktop_preset_adapter if column_exists?(:users, :desktop_preset_adapter)
20
+ end
21
+
22
+ private
23
+
24
+ def backfill_legacy_desktop_presets
25
+ PRESET_EMAILS.each do |adapter, email|
26
+ execute <<~SQL.squish
27
+ UPDATE users
28
+ SET desktop_preset_adapter = #{connection.quote(adapter)}
29
+ WHERE desktop_preset_adapter IS NULL
30
+ AND email = #{connection.quote(email)}
31
+ AND llm_vendor = 'cli_proxy'
32
+ AND agent_gateway_id IN (
33
+ SELECT id FROM agent_gateways WHERE desktop_managed = TRUE
34
+ )
35
+ SQL
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MakeAgentGatewayCompletionKeyNullable < ActiveRecord::Migration[8.0]
4
+ def change
5
+ change_column_null :agent_gateways, :completion_key, true
6
+ end
7
+ end
@@ -0,0 +1,101 @@
1
+ class RemoveWorkflowColumnsFromTasks < ActiveRecord::Migration[8.1]
2
+ # The /work command and its WorkflowExecutor are gone, and they were the only
3
+ # producers of parent_task_id / workflow_context / workflow_state / retry_count.
4
+ # Nothing reads these columns anymore, so drop them.
5
+
6
+ IN_FLIGHT_STATUSES = %w[pending queued running delegated pending_approval].freeze
7
+
8
+ # AiAgentJob reserves the agent's ResourceTracker slot under task.id once the
9
+ # job starts, so only these statuses can still hold one; pending and queued
10
+ # never reached reserve!. release! on an unreserved id is a no-op anyway.
11
+ RESERVED_STATUSES = %w[running delegated pending_approval].freeze
12
+
13
+ def up
14
+ cancel_in_flight_workflow_tasks
15
+
16
+ remove_foreign_key :tasks, :tasks, column: :parent_task_id, if_exists: true
17
+ remove_column :tasks, :parent_task_id, :integer
18
+ remove_column :tasks, :workflow_context, :text
19
+ remove_column :tasks, :workflow_state, :json
20
+ remove_column :tasks, :retry_count, :integer, default: 0, null: false
21
+ end
22
+
23
+ def down
24
+ add_column :tasks, :parent_task_id, :integer
25
+ add_column :tasks, :workflow_context, :text
26
+ add_column :tasks, :workflow_state, :json
27
+ add_column :tasks, :retry_count, :integer, default: 0, null: false
28
+ add_index :tasks, :parent_task_id
29
+ add_foreign_key :tasks, :tasks, column: :parent_task_id, on_delete: :nullify
30
+ end
31
+
32
+ private
33
+
34
+ # Any workflow still mid-flight loses the state that would let it advance.
35
+ # Cancel those rows before the columns disappear so they cannot linger as
36
+ # permanently "running" tasks holding a topic slot.
37
+ def cancel_in_flight_workflow_tasks
38
+ rows = select_all(<<~SQL.squish).to_a
39
+ SELECT id, agent_id, topic_id, creative_id, status
40
+ FROM tasks
41
+ WHERE status IN (#{quoted_in_flight_statuses})
42
+ AND (parent_task_id IS NOT NULL
43
+ OR trigger_event_name IN ('work_command', 'workflow_subtask'))
44
+ SQL
45
+ return if rows.empty?
46
+
47
+ ids = rows.map { |row| row["id"].to_i }.join(", ")
48
+ execute(<<~SQL.squish)
49
+ UPDATE tasks
50
+ SET status = 'cancelled', updated_at = #{connection.quote(Time.current)}
51
+ WHERE id IN (#{ids})
52
+ SQL
53
+
54
+ release_slots(rows)
55
+ end
56
+
57
+ # A raw UPDATE only settles the row. Every other bulk cancellation in the app
58
+ # (TasksController#cancel, CancelOfflineDelegatedTasksJob, stuck recovery)
59
+ # also hands back the agent's ResourceTracker slot and drains the topic queue,
60
+ # because these tasks were paused with no live worker to run AiAgentJob's
61
+ # ensure-block. Skipping that here would keep a cancelled task counting
62
+ # against the agent's max_concurrent_jobs until the cache entry expires an
63
+ # hour later — long enough, with a limit of 1, to freeze the agent right after
64
+ # deploy. Reaching into app classes from a migration is unusual, but the
65
+ # reservation lives in Rails.cache and there is no SQL equivalent.
66
+ def release_slots(rows)
67
+ return unless defined?(Collavre::Orchestration::ResourceTracker)
68
+
69
+ rows.group_by { |row| row["agent_id"] }.each do |agent_id, agent_rows|
70
+ next if agent_id.blank?
71
+
72
+ agent = Collavre::User.find_by(id: agent_id)
73
+ next unless agent
74
+
75
+ tracker = Collavre::Orchestration::ResourceTracker.for(agent)
76
+ agent_rows.each do |row|
77
+ tracker.release!(row["id"]) if RESERVED_STATUSES.include?(row["status"])
78
+ end
79
+ end
80
+
81
+ drain_topics(rows)
82
+ rescue StandardError => e
83
+ # Cache/orchestration cleanup is best effort: stuck recovery still sweeps
84
+ # whatever is left. Never fail the schema change over it.
85
+ say("Slot release after workflow cancellation failed: #{e.class}: #{e.message}")
86
+ end
87
+
88
+ # Freeing the slot is only half of it — a waiter queued behind the cancelled
89
+ # holder stays queued until stuck recovery notices, which can take minutes.
90
+ def drain_topics(rows)
91
+ rows.filter_map { |row| [ row["topic_id"], row["creative_id"] ] if row["topic_id"].present? }
92
+ .uniq
93
+ .each do |topic_id, creative_id|
94
+ Collavre::Orchestration::AgentOrchestrator.dequeue_next_for_topic(topic_id, creative_id)
95
+ end
96
+ end
97
+
98
+ def quoted_in_flight_statuses
99
+ IN_FLIGHT_STATUSES.map { |status| connection.quote(status) }.join(", ")
100
+ end
101
+ end
@@ -0,0 +1,24 @@
1
+ class RemoveAndroidDevices < ActiveRecord::Migration[8.0]
2
+ ANDROID_DEVICE_CONSTRAINT = "devices_no_android_device_type".freeze
3
+
4
+ def up
5
+ if connection.supports_validate_constraints?
6
+ # PostgreSQL can enforce a not-yet-validated constraint. This closes the
7
+ # rolling-deploy window before removing existing registrations: an old
8
+ # container may finish a request, but cannot recreate an Android token.
9
+ add_check_constraint :devices, "device_type != 2", name: ANDROID_DEVICE_CONSTRAINT, validate: false
10
+ execute "DELETE FROM devices WHERE device_type = 2"
11
+ validate_check_constraint :devices, name: ANDROID_DEVICE_CONSTRAINT
12
+ else
13
+ # SQLite rebuilds the table to add a check constraint, so it cannot add
14
+ # one while Android rows exist. The migration transaction serializes
15
+ # writers, making the delete and constraint addition atomic here.
16
+ execute "DELETE FROM devices WHERE device_type = 2"
17
+ add_check_constraint :devices, "device_type != 2", name: ANDROID_DEVICE_CONSTRAINT
18
+ end
19
+ end
20
+
21
+ def down
22
+ raise ActiveRecord::IrreversibleMigration, "Android device tokens cannot be restored"
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ class DropInboxItems < ActiveRecord::Migration[8.0]
2
+ def up
3
+ drop_table :inbox_items
4
+ end
5
+
6
+ def down
7
+ create_table :inbox_items do |t|
8
+ t.references :comment, foreign_key: { on_delete: :nullify }
9
+ t.references :creative, foreign_key: { on_delete: :nullify }
10
+ t.text :message
11
+ t.string :message_key
12
+ t.jsonb :message_params, default: {}, null: false
13
+ t.references :owner, null: false, foreign_key: { to_table: :users }
14
+ t.string :link
15
+ t.string :state, default: "new", null: false
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :inbox_items, :state
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ClearClaudeChannelPresenceRoutingExpressions < ActiveRecord::Migration[8.0]
4
+ def up
5
+ execute <<~SQL.squish
6
+ UPDATE users
7
+ SET routing_expression = NULL
8
+ WHERE llm_model = 'claude-code'
9
+ AND routing_expression = 'true'
10
+ SQL
11
+ end
12
+
13
+ def down
14
+ # Presence rows, not routing_expression, now own Claude Channel liveness.
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ class AddLastTopicRevisionToUserCreativePreferences < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :user_creative_preferences, :last_topic_revision, :integer, null: false, default: 0
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddLastTopicSaveOrderToUserCreativePreferences < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :user_creative_preferences, :last_topic_save_session_id, :string
4
+ add_column :user_creative_preferences, :last_topic_save_sequence, :integer
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddLastTopicSaveSequencesToUserCreativePreferences < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :user_creative_preferences, :last_topic_save_sequences, :json, null: false, default: {}
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddLastTopicSaveFencesToUserCreativePreferences < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :user_creative_preferences, :last_topic_save_fence_issued, :bigint, null: false, default: 0
4
+ add_column :user_creative_preferences, :last_topic_save_fence_applied, :bigint, null: false, default: 0
5
+ end
6
+ end
@@ -0,0 +1,79 @@
1
+ class AddTopicToCommentReadPointers < ActiveRecord::Migration[8.0]
2
+ def up
3
+ add_reference :comment_read_pointers, :topic,
4
+ foreign_key: { to_table: :topics, on_delete: :cascade }, null: true
5
+ remove_index :comment_read_pointers, column: %i[user_id creative_id]
6
+
7
+ execute <<~SQL.squish
8
+ INSERT INTO comment_read_pointers
9
+ (user_id, creative_id, topic_id, last_read_comment_id, created_at, updated_at)
10
+ SELECT pointers.user_id, pointers.creative_id, topics.id,
11
+ pointers.last_read_comment_id, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
12
+ FROM comment_read_pointers AS pointers
13
+ INNER JOIN topics ON topics.creative_id = pointers.creative_id
14
+ WHERE pointers.topic_id IS NULL
15
+ SQL
16
+
17
+ add_index :comment_read_pointers, %i[user_id creative_id topic_id], unique: true,
18
+ name: "index_comment_read_pointers_on_user_creative_and_topic"
19
+ add_index :comment_read_pointers, %i[user_id creative_id], unique: true,
20
+ where: "topic_id IS NULL", name: "index_comment_read_pointers_on_legacy_pointer"
21
+ end
22
+
23
+ def down
24
+ remove_index :comment_read_pointers, name: "index_comment_read_pointers_on_legacy_pointer"
25
+ remove_index :comment_read_pointers, name: "index_comment_read_pointers_on_user_creative_and_topic"
26
+ consolidate_topic_watermarks
27
+ execute "DELETE FROM comment_read_pointers WHERE topic_id IS NOT NULL"
28
+ remove_reference :comment_read_pointers, :topic, foreign_key: true
29
+ add_index :comment_read_pointers, %i[user_id creative_id], unique: true
30
+ end
31
+
32
+ private
33
+
34
+ def consolidate_topic_watermarks
35
+ create_missing_legacy_pointers
36
+
37
+ execute <<~SQL.squish
38
+ UPDATE comment_read_pointers AS legacy_pointers
39
+ SET last_read_comment_id = COALESCE((
40
+ SELECT MIN(COALESCE(topic_pointers.last_read_comment_id, 0))
41
+ FROM topics
42
+ LEFT JOIN comment_read_pointers AS topic_pointers
43
+ ON topic_pointers.user_id = legacy_pointers.user_id
44
+ AND topic_pointers.creative_id = legacy_pointers.creative_id
45
+ AND topic_pointers.topic_id = topics.id
46
+ WHERE topics.creative_id = legacy_pointers.creative_id
47
+ ), 0)
48
+ WHERE legacy_pointers.topic_id IS NULL
49
+ AND COALESCE(legacy_pointers.last_read_comment_id, 0) > COALESCE((
50
+ SELECT MIN(COALESCE(topic_pointers.last_read_comment_id, 0))
51
+ FROM topics
52
+ LEFT JOIN comment_read_pointers AS topic_pointers
53
+ ON topic_pointers.user_id = legacy_pointers.user_id
54
+ AND topic_pointers.creative_id = legacy_pointers.creative_id
55
+ AND topic_pointers.topic_id = topics.id
56
+ WHERE topics.creative_id = legacy_pointers.creative_id
57
+ ), 0)
58
+ SQL
59
+ end
60
+
61
+ def create_missing_legacy_pointers
62
+ execute <<~SQL.squish
63
+ INSERT INTO comment_read_pointers (user_id, creative_id, last_read_comment_id, created_at, updated_at)
64
+ SELECT pointer_pairs.user_id, pointer_pairs.creative_id,
65
+ 0,
66
+ CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
67
+ FROM (
68
+ SELECT DISTINCT user_id, creative_id
69
+ FROM comment_read_pointers
70
+ WHERE topic_id IS NOT NULL
71
+ ) AS pointer_pairs
72
+ LEFT JOIN comment_read_pointers AS legacy_pointers
73
+ ON legacy_pointers.user_id = pointer_pairs.user_id
74
+ AND legacy_pointers.creative_id = pointer_pairs.creative_id
75
+ AND legacy_pointers.topic_id IS NULL
76
+ WHERE legacy_pointers.id IS NULL
77
+ SQL
78
+ end
79
+ end
@@ -0,0 +1,37 @@
1
+ class AddCreativeTopicPrivateIdIndexToComments < ActiveRecord::Migration[8.1]
2
+ # Badge fanout counts per-topic suffixes after a reader watermark. The former
3
+ # creative/private/id index still needs to scan every newer topic in a busy
4
+ # creative, while this index lets the database seek directly to that topic's
5
+ # suffix.
6
+ disable_ddl_transaction!
7
+
8
+ def up
9
+ options = { name: index_name, if_not_exists: true }
10
+
11
+ if postgresql?
12
+ options.merge!(where: "private = false", algorithm: :concurrently)
13
+ add_index :comments, %i[creative_id topic_id id], **options
14
+ else
15
+ add_index :comments, %i[creative_id private topic_id id], **options
16
+ end
17
+ end
18
+
19
+ def down
20
+ options = { name: index_name, algorithm: concurrent_algorithm, if_exists: true }
21
+ remove_index :comments, **options
22
+ end
23
+
24
+ private
25
+
26
+ def index_name
27
+ "index_comments_on_creative_topic_private_and_id"
28
+ end
29
+
30
+ def postgresql?
31
+ connection.adapter_name.match?(/postgres/i)
32
+ end
33
+
34
+ def concurrent_algorithm
35
+ :concurrently if postgresql?
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ class AddWatermarkIndexToCommentReadPointers < ActiveRecord::Migration[8.1]
2
+ # Both receipt paths — rendering a page and repainting after a read — select a
3
+ # creative's pointers by watermark range. The existing indexes are keyed by
4
+ # user first, so those range scans read every pointer row on the creative and
5
+ # filtered in memory. Pointer rows are now per user *and* per topic, so that
6
+ # set grows multiplicatively.
7
+ disable_ddl_transaction!
8
+
9
+ def up
10
+ add_index :comment_read_pointers, %i[creative_id last_read_comment_id],
11
+ name: index_name, if_not_exists: true, algorithm: concurrent_algorithm
12
+ end
13
+
14
+ def down
15
+ remove_index :comment_read_pointers, name: index_name, if_exists: true, algorithm: concurrent_algorithm
16
+ end
17
+
18
+ private
19
+
20
+ def index_name
21
+ "index_comment_read_pointers_on_creative_and_watermark"
22
+ end
23
+
24
+ def concurrent_algorithm
25
+ :concurrently if connection.adapter_name.match?(/postgres/i)
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ class AddPrivateRecipientTopicIndexToComments < ActiveRecord::Migration[8.1]
2
+ # Badge fanout asks which topics hold private comments for each recipient. The
3
+ # existing private index is keyed by topic before recipient, so answering that
4
+ # per user meant scanning every private comment on the creative.
5
+ disable_ddl_transaction!
6
+
7
+ RECIPIENT_COLUMNS = %i[user_id approver_id].freeze
8
+
9
+ def up
10
+ RECIPIENT_COLUMNS.each do |recipient_column|
11
+ add_index :comments, index_columns(recipient_column), **index_options(recipient_column).merge(if_not_exists: true)
12
+ end
13
+ end
14
+
15
+ def down
16
+ RECIPIENT_COLUMNS.each do |recipient_column|
17
+ remove_index :comments, name: index_name(recipient_column), if_exists: true, algorithm: concurrent_algorithm
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # On PostgreSQL the predicate carries `private`, so it stays out of the key.
24
+ def index_columns(recipient_column)
25
+ return [ :creative_id, recipient_column, :topic_id ] if postgresql?
26
+
27
+ [ :creative_id, :private, recipient_column, :topic_id ]
28
+ end
29
+
30
+ def index_options(recipient_column)
31
+ options = { name: index_name(recipient_column) }
32
+ return options unless postgresql?
33
+
34
+ options.merge(where: "private = true", algorithm: :concurrently)
35
+ end
36
+
37
+ def index_name(recipient_column)
38
+ "index_comments_on_creative_private_#{recipient_column}_and_topic"
39
+ end
40
+
41
+ def postgresql?
42
+ connection.adapter_name.match?(/postgres/i)
43
+ end
44
+
45
+ def concurrent_algorithm
46
+ :concurrently if postgresql?
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ class AddTopicAssignedAtToComments < ActiveRecord::Migration[8.1]
2
+ def up
3
+ add_column :comments, :topic_assigned_at, :datetime, precision: 6
4
+ execute "UPDATE comments SET topic_assigned_at = created_at"
5
+ change_column_null :comments, :topic_assigned_at, false
6
+ add_index :comments, %i[topic_id topic_assigned_at], name: "index_comments_on_topic_and_assigned_at"
7
+ end
8
+
9
+ def down
10
+ remove_index :comments, name: "index_comments_on_topic_and_assigned_at"
11
+ remove_column :comments, :topic_assigned_at
12
+ end
13
+ end
data/db/seeds.rb CHANGED
@@ -17,54 +17,4 @@ module Collavre
17
17
  end
18
18
 
19
19
  Collavre::ChannelBotSeed.call
20
-
21
- module Collavre
22
- # Seeds the "Typo Corrector" agent. Unlike event-routed agents (e.g. the GitHub
23
- # PR Analyzer) this agent has no routing_expression — it is never dispatched by
24
- # comment events. It exists purely as a user-configurable holder for the LLM
25
- # vendor/model/system_prompt that Collavre::TypoCorrector invokes directly.
26
- module TypoCorrectorSeed
27
- AGENT_EMAIL = "typo-corrector@collavre.local"
28
-
29
- SYSTEM_PROMPT = <<~PROMPT
30
- You are a typo correction engine. You receive a short piece of text the user
31
- is currently typing and return ONLY spelling/typo fixes as a structured edit list.
32
-
33
- Rules:
34
- - Fix only spelling mistakes and obvious typos (transposed/missing/extra letters,
35
- wrong jamo, mis-spaced words). Works for any language (ko, en, ja, zh, mixed).
36
- - Do NOT rewrite style, grammar, word choice, tone, or punctuation.
37
- - Do NOT change meaning. Do NOT translate.
38
- - Never touch code spans, URLs, @mentions, emojis, or HTML/markdown markup.
39
- - `original` MUST be an exact substring copied verbatim from the input text.
40
- - Keep each edit minimal: a single word or short phrase, not a whole sentence.
41
-
42
- Return ONLY valid JSON, no markdown, in exactly this shape:
43
- {"edits":[{"original":"<verbatim>","suggestion":"<fixed>","reason":"spelling","confidence":0.0}]}
44
-
45
- `confidence` is a float 0.0-1.0 for how sure you are it is a real typo.
46
- If there are no typos, return {"edits":[]}.
47
- PROMPT
48
-
49
- def self.call
50
- user = User.find_or_initialize_by(email: AGENT_EMAIL)
51
- return user unless user.new_record?
52
-
53
- user.assign_attributes(
54
- name: "Typo Corrector",
55
- password: SecureRandom.hex(32),
56
- email_verified_at: Time.current,
57
- searchable: false,
58
- llm_vendor: ENV.fetch("COLLAVRE_DEFAULT_LLM_VENDOR", "gemini"),
59
- llm_model: ENV.fetch("COLLAVRE_DEFAULT_LLM_MODEL", "gemini-3.1-flash-lite"),
60
- system_prompt: SYSTEM_PROMPT,
61
- tools: []
62
- )
63
- user.save!
64
- Rails.logger.info "[Collavre] Typo Corrector agent ensured: #{AGENT_EMAIL}"
65
- user
66
- end
67
- end
68
- end
69
-
70
- Collavre::TypoCorrectorSeed.call
20
+ Collavre::LlmModel.seed_default_suggestions!