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
@@ -23,10 +23,16 @@ ko:
23
23
  vendor_label: LLM 벤더
24
24
  vendor_help: 이 에이전트에 사용할 AI 제공업체를 선택하세요
25
25
  model_label: 모델
26
+ remove_model: 모델 목록에서 삭제
27
+ remove_model_failed: 모델을 목록에서 삭제하지 못했습니다.
26
28
  api_key_label: API Key
27
29
  api_key_help: 입력하면 시스템 기본값 대신 이 키가 사용됩니다. 암호화되어 저장됩니다.
28
30
  gateway_url_label: Gateway URL
29
31
  gateway_url_help: OpenClaw 사용 시 필수입니다. OpenClaw 게이트웨이의 URL을 입력하세요.
32
+ agent_gateway_label: Agent Gateway
33
+ agent_gateway_blank: CLI Proxy 게이트웨이를 선택하세요
34
+ agent_gateway_help: LLM 벤더가 CLI Proxy일 때만 필요합니다.
35
+ manage_gateways: 게이트웨이 관리
30
36
  searchable_label: 이 AI를 검색 및 멘션 가능하게 허용
31
37
  searchable_help: 활성화하면 다른 사용자가 이름으로 이 AI를 찾고 멘션할 수 있습니다.
32
38
  submit: AI 에이전트 추가
@@ -51,7 +57,12 @@ ko:
51
57
  not_an_ai: 이 사용자는 AI 에이전트가 아닙니다.
52
58
  tools_title: 사용 가능한 도구
53
59
  tools_description: 에이전트가 사용할 수 있는 도구 목록입니다.
60
+ meta_skills_title: Collavre 메타 스킬
61
+ meta_skills_description: 프로비저닝되는 collavre-cli 스킬 하나가 선택된 도구를 메타 스킬로 실행합니다. 현재는 이 선택을 callback token 권한으로 강제하지 않습니다.
62
+ gateway_mode: "워크스페이스 모드: %{mode}. 선택한 게이트웨이 설정에서 변경할 수 있습니다."
54
63
  no_tools_found: 사용 가능한 도구가 없습니다.
64
+ clear_api_key_label: 저장된 API 키 삭제
65
+ clear_api_key_help: 저장 후 시스템 기본 키 또는 키가 필요 없는 게이트웨이를 사용합니다.
55
66
  agent_conf_label: "에이전트 설정 (YAML)"
56
67
  agent_conf_help: "에이전트 동작을 위한 YAML 설정. chat_history: 최대 이전 메시지 수 (기본 50), chat_history_size: 최대 총 문자 수 (기본 100000), context.creative_children_level: 크리에이티브 컨텍스트에 포함할 자식 깊이 (기본 nil → 사용자 설정 따름, 0=자식 없음, 1=직접 자식, 2=손자까지)."
57
68
  chat_title: 에이전트와 대화하기
@@ -111,25 +122,14 @@ ko:
111
122
  theme: 테마
112
123
  calendar_id: 캘린더 ID
113
124
  notifications_enabled: 알림 사용
125
+ creative_workspace:
126
+ enabled: 3단 크리에이티브 작업 공간 사용
127
+ description: 왼쪽 크리에이티브 트리를 유지하고 전체 페이지 새로고침 없이 중앙 내용을 전환하며 오른쪽에 채팅을 표시합니다.
114
128
  locale: 언어
115
129
  locales:
116
130
  en: 영어
117
131
  ko: 한국어
118
132
  timezone: 시간대
119
- typo_correction:
120
- legend: 오타 자동 수정
121
- manage: 오타 자동 수정 설정
122
- description: 입력하는 동안 오타를 자동으로 제안하고 수정합니다. 아래의 타이핑 장치와 입력 위치가 모두 켜져 있을 때만 동작합니다.
123
- enabled: 오타 자동 수정 사용
124
- threshold: 자동 적용 기준
125
- threshold_hint: 이 신뢰도(0-100) 이상이면 자동으로 적용되고, 낮으면 후보로만 표시됩니다.
126
- device_legend: 타이핑 장치
127
- on_voice: 음성 입력
128
- on_soft_keyboard: 화면(소프트) 키보드
129
- on_physical_keyboard: 물리 키보드(블루투스/유선)
130
- location_legend: 입력 위치
131
- in_chat: 채팅 메시지 입력
132
- in_editor: 크리에이티브 편집기
133
133
  profile_updated: 프로파일이 업데이트되었습니다.
134
134
  avatar_updated: 아바타가 변경되었습니다.
135
135
  password_updated: 비밀번호가 성공적으로 변경되었습니다.
data/config/routes.rb CHANGED
@@ -2,9 +2,26 @@ Collavre::Engine.routes.draw do
2
2
  # Landing page
3
3
  get "landing", to: "landing#show"
4
4
 
5
+ # Public feature guides — the long-form counterpart to the empty-chat feature
6
+ # cards. Keys come from Collavre::FeatureCardRegistry, which rejects a key this
7
+ # constraint could not route, so the two cannot drift apart.
8
+ get "features", to: "features#index", as: :features
9
+ get "features/:key", to: "features#show", as: :feature,
10
+ constraints: { key: Collavre::FeatureCard::GUIDE_KEY_FORMAT }
11
+
5
12
  # Authentication routes
6
13
  resource :session, only: [ :new, :create, :destroy ]
7
14
  resources :passwords, param: :token, only: [ :new, :create, :edit, :update ]
15
+ resources :agent_gateways, path: "settings/agent-gateways", except: :show do
16
+ post :check, on: :member
17
+ end
18
+ get "desktop/setup", to: "desktop_setup#show", as: :desktop_setup
19
+ post "desktop/setup/account", to: "desktop_setup#create_account", as: :desktop_setup_account
20
+ post "desktop/setup/registration-token", to: "desktop_setup#registration_token", as: :desktop_setup_registration_token
21
+ get "desktop/setup/sidecar-health", to: "desktop_sidecar_health#show", as: :desktop_setup_sidecar_health
22
+ post "desktop/setup/validate-registration-grant", to: "desktop_gateway_registrations#validate_registration_grant", as: :desktop_setup_validate_registration_grant
23
+ post "desktop/setup/gateway-registered", to: "desktop_gateway_registrations#registered", as: :desktop_setup_gateway_registered
24
+ post "desktop/setup/register-gateway", to: "desktop_gateway_registrations#create", as: :desktop_setup_register_gateway
8
25
  resources :users, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
9
26
  collection do
10
27
  get :new_ai
@@ -23,9 +40,27 @@ Collavre::Engine.routes.draw do
23
40
  get :edit_password
24
41
  patch :update_password
25
42
  get :passkeys
26
- get :typo_correction
43
+ get :agent_connection, to: "agent_connections#show"
27
44
  end
28
45
  end
46
+ scope "users/:user_id/agent-connection", as: :agent_connection do
47
+ get :status, to: "agent_connections#status"
48
+ post "auth/:engine/sessions", to: "agent_connections#create_auth_session", as: :auth_sessions
49
+ get "auth/:engine/sessions/:session_id", to: "agent_connections#auth_session", as: :auth_session
50
+ post "auth/:engine/sessions/:session_id", to: "agent_connections#submit_auth_session"
51
+ delete "auth/:engine/sessions/:session_id", to: "agent_connections#cancel_auth_session"
52
+ post "provision/sync", to: "agent_connections#provision_sync", as: :provision_sync
53
+ post "provision/items/:type/:name/approve", to: "agent_connections#provision_approve", as: :provision_approve
54
+ delete "provision/items/:type/:name", to: "agent_connections#provision_delete", as: :provision_delete
55
+ post "rotate-tokens", to: "agent_connections#rotate_tokens", as: :rotate_tokens
56
+ end
57
+
58
+ get "/agents/:agent_id/workspaces/:token/provision.json",
59
+ to: "agent_provisioning#manifest", as: :agent_provision_manifest, format: false
60
+ get "/agent-provisioning/collavre/:sha256.tar.gz",
61
+ to: "agent_provisioning#skill", as: :agent_provision_skill, format: false
62
+ get "/agents/:agent_id/workspaces/:token/config/:sha256.tar.gz",
63
+ to: "agent_provisioning#config_archive", as: :agent_provision_config, format: false
29
64
  get "/email_verification/:token", to: "email_verifications#show", as: :email_verification
30
65
 
31
66
  # OAuth callback routes (paths match OmniAuth provider names)
@@ -42,20 +77,19 @@ Collavre::Engine.routes.draw do
42
77
  resources :contacts, only: [ :destroy ]
43
78
  resources :devices, only: [ :create ]
44
79
 
45
- resources :inbox_items, path: "inbox", only: [ :index, :update, :destroy ] do
46
- get :count, on: :collection
47
- end
48
-
49
80
  resources :user_themes, only: [ :index, :create, :destroy ] do
50
81
  member do
51
82
  post :apply
52
83
  end
53
84
  end
54
85
 
55
- resources :typo_corrections, only: [ :create ]
86
+ resources :llm_models, only: [ :destroy ]
56
87
 
57
88
  resources :creative_imports, only: [ :create ]
58
89
  resources :tasks, only: [] do
90
+ collection do
91
+ get :active_statuses
92
+ end
59
93
  member do
60
94
  post :cancel
61
95
  end
@@ -115,17 +149,18 @@ Collavre::Engine.routes.draw do
115
149
  collection do
116
150
  post :reorder
117
151
  post :link_drop
152
+ patch :next_last_visited_sequence
118
153
  get :append_as_parent, to: "creatives#append_as_parent", as: :append_as_parent_creative
119
154
  get :append_below, to: "creatives#append_below", as: :append_below_creative
120
155
  get :export_markdown
121
156
  end
122
157
  member do
158
+ patch :remember_last_visited
123
159
  get :children
124
160
  post :request_permission, to: "creatives#request_permission"
125
161
  post :unconvert
126
162
  patch :archive
127
163
  patch :unarchive
128
- get :parent_suggestions
129
164
  get :slide_view
130
165
  get :contexts
131
166
  patch :update_contexts
@@ -136,10 +171,11 @@ Collavre::Engine.routes.draw do
136
171
 
137
172
  resources :emails, only: [ :index, :show ]
138
173
  resource :invite, only: [ :show, :create ]
139
-
140
174
  post "/creative_expanded_states/toggle", to: "user_creative_preferences#toggle"
141
- patch "/creatives/:creative_id/user_creative_preferences/update_last_topic", to: "user_creative_preferences#update_last_topic", as: :update_last_topic
175
+ match "/creatives/:creative_id/user_creative_preferences/update_last_topic", to: "user_creative_preferences#update_last_topic", via: [ :post, :patch ], as: :update_last_topic
142
176
  post "/comment_read_pointers/update", to: "comment_read_pointers#update"
177
+ post "/notices/:key/dismiss", to: "notices#dismiss", as: :dismiss_notice
178
+ delete "/notices", to: "notices#restore_all", as: :restore_notices
143
179
 
144
180
  # Agent API (Claude Channel MCP plugin)
145
181
  namespace :api do
@@ -148,16 +184,6 @@ Collavre::Engine.routes.draw do
148
184
  post "agent/reply", to: "agents#reply"
149
185
  post "agent/notify", to: "agents#notify"
150
186
  delete "agent/:id", to: "agents#destroy"
151
-
152
- # Mobile voice companion (Android): poll Inbox#System messages, read aloud,
153
- # reply to the origin topic; a cold mic press starts work in Inbox#Main.
154
- namespace :mobile do
155
- post "voice_commands", to: "voice_commands#create"
156
- get "agent_events", to: "agent_events#index"
157
- post "agent_events/:id/respond", to: "agent_events#respond"
158
- post "agent_events/:id/read", to: "agent_events#read"
159
- post "devices", to: "devices#create"
160
- end
161
187
  end
162
188
  end
163
189
 
@@ -2,7 +2,7 @@ class BackfillCreativeInCommentInboxItems < ActiveRecord::Migration[8.0]
2
2
  disable_ddl_transaction!
3
3
 
4
4
  def up
5
- InboxItem.where(message_key: [ "inbox.comment_added", "inbox.user_mentioned" ]).find_each do |item|
5
+ inbox_item_class.where(message_key: [ "inbox.comment_added", "inbox.user_mentioned" ]).find_each do |item|
6
6
  params = item.message_params || {}
7
7
  next if params["creative"].present?
8
8
 
@@ -24,4 +24,12 @@ class BackfillCreativeInCommentInboxItems < ActiveRecord::Migration[8.0]
24
24
  def down
25
25
  # no-op
26
26
  end
27
+
28
+ private
29
+
30
+ def inbox_item_class
31
+ Class.new(ActiveRecord::Base) do
32
+ self.table_name = "inbox_items"
33
+ end
34
+ end
27
35
  end
@@ -0,0 +1,34 @@
1
+ class AddCreativeIdIndexToComments < ActiveRecord::Migration[8.1]
2
+ # The chat timeline now orders by comments.id (not created_at) to survive
3
+ # cross-process clock skew. A composite index on [creative_id, id] lets the DB
4
+ # satisfy the per-creative filter and the id-ordered LIMIT/range paging from
5
+ # the index, instead of scanning creative_id then sorting by id.
6
+ #
7
+ # Built CONCURRENTLY on PostgreSQL so the deploy never takes a write-blocking
8
+ # lock on the hot comments table while the index builds. Requires running
9
+ # outside a transaction. SQLite (dev/test) has no concurrent algorithm, so we
10
+ # omit it there; if_not_exists keeps the build idempotent on both.
11
+ disable_ddl_transaction!
12
+
13
+ # Explicit up/down: `change` auto-reverses add_index by forwarding every option
14
+ # (including if_not_exists:) to remove_index, which rejects it and fails rollback.
15
+ def up
16
+ add_index :comments, [ :creative_id, :id ],
17
+ name: "index_comments_on_creative_id_and_id",
18
+ algorithm: concurrent_algorithm,
19
+ if_not_exists: true
20
+ end
21
+
22
+ def down
23
+ remove_index :comments,
24
+ name: "index_comments_on_creative_id_and_id",
25
+ algorithm: concurrent_algorithm,
26
+ if_exists: true
27
+ end
28
+
29
+ private
30
+
31
+ def concurrent_algorithm
32
+ :concurrently if connection.adapter_name.match?(/postgres/i)
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ class AddCreativePrivateIdIndexToComments < ActiveRecord::Migration[8.1]
2
+ # Every unread/badge count is "comments of this creative, public only, newer
3
+ # than a watermark" — `WHERE creative_id = ? AND private = false [AND id > ?]`.
4
+ # index_comments_on_creative_id_and_id can serve the creative filter and the
5
+ # id ordering but not the private filter, so the planner still had to visit
6
+ # every row of a busy creative to discard private ones.
7
+ #
8
+ # Production PostgreSQL gets the smallest useful structure: a partial index
9
+ # over public rows only. SQLite renders `where(private: false)` as
10
+ # `private = 0`, which does not use a partial predicate recorded as
11
+ # `private = false`, so dev, test, and desktop use an equivalent three-column
12
+ # index instead.
13
+ #
14
+ # index_comments_on_creative_id_and_id stays: paging does not include the
15
+ # public-only predicate, and SQLite cannot skip the fallback index's leading
16
+ # `private` column for a plain `ORDER BY id` query.
17
+ disable_ddl_transaction!
18
+
19
+ # Explicit up/down: `change` auto-reverses add_index by forwarding every option
20
+ # (including if_not_exists:) to remove_index, which rejects it and fails rollback.
21
+ def up
22
+ options = { name: index_name, if_not_exists: true }
23
+
24
+ if postgresql?
25
+ options.merge!(where: "private = false", algorithm: :concurrently)
26
+ add_index :comments, [ :creative_id, :id ], **options
27
+ else
28
+ add_index :comments, [ :creative_id, :private, :id ], **options
29
+ end
30
+ end
31
+
32
+ def down
33
+ options = { name: index_name, algorithm: concurrent_algorithm, if_exists: true }
34
+ remove_index :comments, **options
35
+ end
36
+
37
+ private
38
+
39
+ def index_name
40
+ "index_comments_on_creative_id_and_private_and_id"
41
+ end
42
+
43
+ def postgresql?
44
+ connection.adapter_name.match?(/postgres/i)
45
+ end
46
+
47
+ def concurrent_algorithm
48
+ :concurrently if postgresql?
49
+ end
50
+ end
@@ -0,0 +1,64 @@
1
+ class AddAsyncCommentNotificationState < ActiveRecord::Migration[8.1]
2
+ disable_ddl_transaction!
3
+
4
+ def up
5
+ add_column :comments, :notification_key, :string unless column_exists?(:comments, :notification_key)
6
+ unless column_exists?(:comments, :notification_revision)
7
+ add_column :comments, :notification_revision, :integer, default: 0, null: false
8
+ end
9
+
10
+ create_table :comment_notification_deliveries, if_not_exists: true do |t|
11
+ t.string :delivery_key, null: false
12
+ t.bigint :inbox_comment_id, null: false
13
+ t.bigint :recipient_id, null: false
14
+ t.text :message, null: false
15
+ t.text :link
16
+ t.string :push_claim_token
17
+ t.datetime :push_claimed_at
18
+ t.datetime :push_enqueued_at
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :comments,
23
+ :notification_key,
24
+ unique: true,
25
+ where: "notification_key IS NOT NULL",
26
+ algorithm: concurrent_algorithm,
27
+ if_not_exists: true
28
+
29
+ add_index :comment_notification_deliveries,
30
+ :delivery_key,
31
+ unique: true,
32
+ algorithm: concurrent_algorithm,
33
+ if_not_exists: true
34
+ add_index :comment_notification_deliveries,
35
+ [ :push_enqueued_at, :push_claimed_at ],
36
+ name: "index_comment_notification_deliveries_pending",
37
+ algorithm: concurrent_algorithm,
38
+ if_not_exists: true
39
+ end
40
+
41
+ def down
42
+ remove_index :comment_notification_deliveries,
43
+ name: "index_comment_notification_deliveries_pending",
44
+ algorithm: concurrent_algorithm,
45
+ if_exists: true
46
+ remove_index :comment_notification_deliveries,
47
+ :delivery_key,
48
+ algorithm: concurrent_algorithm,
49
+ if_exists: true
50
+ remove_index :comments,
51
+ :notification_key,
52
+ algorithm: concurrent_algorithm,
53
+ if_exists: true
54
+ drop_table :comment_notification_deliveries, if_exists: true
55
+ remove_column :comments, :notification_revision if column_exists?(:comments, :notification_revision)
56
+ remove_column :comments, :notification_key if column_exists?(:comments, :notification_key)
57
+ end
58
+
59
+ private
60
+
61
+ def concurrent_algorithm
62
+ :concurrently if connection.adapter_name.match?(/postgres/i)
63
+ end
64
+ end
@@ -0,0 +1,30 @@
1
+ # Record what a "⏳ waiting on the topic slot" notice speaks for, instead of
2
+ # inferring it from whichever sibling notices happen to still stand.
3
+ #
4
+ # A topic gets one deduplicated notice while coalescing is on and one notice per
5
+ # deferral while it is off, so the two kinds coexist whenever the policy changes
6
+ # mid-wait or differs per agent. Reading "no sibling notice left" as "this one
7
+ # stood for the whole topic" then inverts ownership: deleting the shared notice
8
+ # is classified as deleting a per-deferral one and cancels the newest queued
9
+ # waiter — which may be the one the *other* notice speaks for.
10
+ #
11
+ # Nullable with no backfill on purpose: a notice already on screen was posted by
12
+ # the pre-existing code and there is nothing on the row to say which kind it was.
13
+ # Comment#cancel_queued_tasks_for_waiting_notice keeps its old behaviour for
14
+ # those, so an in-flight wait is neither widened nor silently disarmed.
15
+ class AddWaitingNoticeScopeToComments < ActiveRecord::Migration[8.1]
16
+ def up
17
+ unless column_exists?(:comments, :waiting_notice_scope)
18
+ add_column :comments, :waiting_notice_scope, :string
19
+ end
20
+
21
+ unless column_exists?(:comments, :waiting_notice_task_id)
22
+ add_column :comments, :waiting_notice_task_id, :integer
23
+ end
24
+ end
25
+
26
+ def down
27
+ remove_column :comments, :waiting_notice_task_id if column_exists?(:comments, :waiting_notice_task_id)
28
+ remove_column :comments, :waiting_notice_scope if column_exists?(:comments, :waiting_notice_scope)
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # Record which kind of "⏳ waiting" notice speaks for a waiter on the waiter
2
+ # itself, at the moment it is parked.
3
+ #
4
+ # The waiter and its notice commit in two steps, so asking the notices which
5
+ # waiters they own has a window where the answer is wrong: an opted-out waiter is
6
+ # already queued while its own notice does not exist yet, and a shared notice
7
+ # deleted in that window reads it as one of its own and cancels it. What speaks
8
+ # for a waiter is decided by the agent's coalescing policy when the row is
9
+ # created, not by which neighbouring rows happen to be visible later.
10
+ #
11
+ # Nullable with no backfill on purpose: a waiter parked by the pre-existing code
12
+ # says nothing about which notice spoke for it, and
13
+ # Comment#represented_queued_waiters keeps the old sibling-notice answer for
14
+ # those rows. Widening would discard work; narrowing would leave an in-flight
15
+ # wait with no stop control.
16
+ class AddWaitingNoticeScopeToTasks < ActiveRecord::Migration[8.1]
17
+ def up
18
+ return if column_exists?(:tasks, :waiting_notice_scope)
19
+
20
+ add_column :tasks, :waiting_notice_scope, :string
21
+ end
22
+
23
+ def down
24
+ remove_column :tasks, :waiting_notice_scope if column_exists?(:tasks, :waiting_notice_scope)
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ class AddCreativeWorkspaceEnabledToUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :users, :creative_workspace_enabled, :boolean, default: false, null: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddDismissedNoticesToUsers < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :users, :dismissed_notices, :json
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ class ChangeCreativeWorkspaceEnabledDefaultToTrue < ActiveRecord::Migration[8.0]
2
+ # Lightweight stub so the backfill does not depend on the app model.
3
+ class MigrationUser < ActiveRecord::Base
4
+ self.table_name = :users
5
+ end
6
+
7
+ def up
8
+ change_column_default :users, :creative_workspace_enabled, from: false, to: true
9
+
10
+ # The preference shipped as opt-in only days ago, so every stored `false`
11
+ # is the old default rather than a deliberate opt-out.
12
+ MigrationUser.where(creative_workspace_enabled: false).update_all(creative_workspace_enabled: true)
13
+ end
14
+
15
+ def down
16
+ raise ActiveRecord::IrreversibleMigration, "cannot distinguish backfilled defaults from post-migration user opt-ins"
17
+ end
18
+ end
@@ -0,0 +1,64 @@
1
+ class CreateLlmModels < ActiveRecord::Migration[8.1]
2
+ MAX_SUGGESTIONS = 100
3
+ MAX_VENDOR_LENGTH = 255
4
+ MAX_NAME_LENGTH = 255
5
+
6
+ DEFAULT_MODELS = {
7
+ "google" => [
8
+ "gemini-3.1-flash-lite",
9
+ "gemini-1.5-flash",
10
+ "gemini-1.5-pro"
11
+ ]
12
+ }.freeze
13
+
14
+ def up
15
+ create_table :llm_models do |t|
16
+ t.string :llm_vendor, null: false, limit: MAX_VENDOR_LENGTH
17
+ t.string :name, null: false, limit: MAX_NAME_LENGTH
18
+ t.references :created_by,
19
+ null: true,
20
+ foreign_key: { to_table: :users, on_delete: :nullify }
21
+ t.timestamps
22
+ end
23
+
24
+ add_index :llm_models, [ :llm_vendor, :name ], unique: true
25
+
26
+ llm_model_class = Class.new(ActiveRecord::Base) do
27
+ self.table_name = "llm_models"
28
+ end
29
+ user_class = Class.new(ActiveRecord::Base) do
30
+ self.table_name = "users"
31
+ end
32
+
33
+ models = DEFAULT_MODELS.flat_map do |vendor, names|
34
+ names.map { |name| [ vendor, name ] }
35
+ end
36
+ models.concat(
37
+ user_class.where.not(llm_vendor: [ nil, "" ])
38
+ .where.not(llm_model: [ nil, "" ])
39
+ .distinct
40
+ .pluck(:llm_vendor, :llm_model)
41
+ )
42
+
43
+ models.each do |vendor, name|
44
+ vendor = vendor.to_s.strip.downcase
45
+ name = name.to_s.strip
46
+ next if vendor.blank? || name.blank?
47
+ next if vendor.length > MAX_VENDOR_LENGTH || name.length > MAX_NAME_LENGTH
48
+
49
+ llm_model_class.find_or_create_by!(
50
+ llm_vendor: vendor,
51
+ name: name
52
+ )
53
+ end
54
+
55
+ keep_ids = llm_model_class.order(updated_at: :desc, id: :desc)
56
+ .limit(MAX_SUGGESTIONS)
57
+ .select(:id)
58
+ llm_model_class.where.not(id: keep_ids).delete_all
59
+ end
60
+
61
+ def down
62
+ drop_table :llm_models
63
+ end
64
+ end
@@ -0,0 +1,35 @@
1
+ class CreateAgentGatewaysAndWorkspaces < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :agent_gateways do |t|
4
+ t.references :owner, null: false, foreign_key: { to_table: :users }
5
+ t.string :name, null: false
6
+ t.string :base_url, null: false
7
+ t.text :admin_key, null: false
8
+ t.text :completion_key, null: false
9
+ t.text :identity_secret
10
+ t.string :tenant_id, null: false, default: "collavre"
11
+ t.integer :workspace_mode, null: false, default: 0
12
+ t.boolean :active, null: false, default: true
13
+ t.timestamps
14
+ end
15
+ add_index :agent_gateways, [ :owner_id, :name ], unique: true
16
+
17
+ add_reference :users, :agent_gateway, foreign_key: true
18
+
19
+ create_table :agent_workspaces do |t|
20
+ t.references :agent, null: false, foreign_key: { to_table: :users }
21
+ t.references :user, foreign_key: { to_table: :users }
22
+ t.references :agent_gateway, null: false, foreign_key: true
23
+ t.string :proxy_user_id, null: false
24
+ t.string :manifest_token, null: false
25
+ t.text :callback_token, null: false
26
+ t.timestamps
27
+ end
28
+
29
+ add_index :agent_workspaces, :manifest_token, unique: true
30
+ add_index :agent_workspaces, [ :agent_id, :agent_gateway_id ],
31
+ unique: true, where: "user_id IS NULL", name: "idx_agent_workspaces_shared"
32
+ add_index :agent_workspaces, [ :agent_id, :user_id, :agent_gateway_id ],
33
+ unique: true, where: "user_id IS NOT NULL", name: "idx_agent_workspaces_per_user"
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ class HashAgentWorkspaceCallbackTokens < ActiveRecord::Migration[8.0]
6
+ PREFIX = "sha256$"
7
+
8
+ class AgentWorkspaceRecord < ActiveRecord::Base
9
+ self.table_name = "agent_workspaces"
10
+
11
+ encrypts :callback_token, deterministic: false
12
+ end
13
+
14
+ class AccessTokenRecord < ActiveRecord::Base
15
+ self.table_name = "oauth_access_tokens"
16
+ end
17
+
18
+ def up
19
+ each_workspace_token do |plaintext|
20
+ token = AccessTokenRecord.find_by(token: plaintext)
21
+ token&.update_columns(token: encoded(plaintext))
22
+ end
23
+ end
24
+
25
+ def down
26
+ each_workspace_token do |plaintext|
27
+ token = AccessTokenRecord.find_by(token: encoded(plaintext))
28
+ token&.update_columns(token: plaintext)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def each_workspace_token
35
+ AgentWorkspaceRecord.find_each do |workspace|
36
+ plaintext = workspace.callback_token
37
+ yield plaintext if plaintext.present?
38
+ end
39
+ end
40
+
41
+ def encoded(plaintext)
42
+ "#{PREFIX}#{Digest::SHA256.hexdigest(plaintext)}"
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ class EncryptAgentWorkspaceManifestTokens < ActiveRecord::Migration[8.0]
6
+ class AgentWorkspaceRecord < ActiveRecord::Base
7
+ self.table_name = "agent_workspaces"
8
+
9
+ encrypts :manifest_token, deterministic: false
10
+ end
11
+
12
+ def up
13
+ add_column :agent_workspaces, :manifest_token_digest, :string
14
+
15
+ say_with_time "Encrypting agent workspace manifest capabilities" do
16
+ transform_tokens(encrypt: true)
17
+ end
18
+
19
+ change_column_null :agent_workspaces, :manifest_token_digest, false
20
+ remove_index :agent_workspaces, :manifest_token
21
+ add_index :agent_workspaces, :manifest_token_digest, unique: true
22
+ end
23
+
24
+ def down
25
+ remove_index :agent_workspaces, :manifest_token_digest
26
+
27
+ say_with_time "Restoring plaintext agent workspace manifest capabilities" do
28
+ transform_tokens(encrypt: false)
29
+ end
30
+
31
+ remove_column :agent_workspaces, :manifest_token_digest
32
+ add_index :agent_workspaces, :manifest_token, unique: true
33
+ end
34
+
35
+ private
36
+
37
+ def transform_tokens(encrypt:)
38
+ connection = ActiveRecord::Base.connection
39
+ encryptor = AgentWorkspaceRecord.type_for_attribute(:manifest_token)
40
+
41
+ AgentWorkspaceRecord.reset_column_information
42
+ rows = connection.select_all("SELECT id, manifest_token FROM agent_workspaces").to_a
43
+ rows.each do |row|
44
+ raw_token = row.fetch("manifest_token")
45
+ plaintext = encrypted?(raw_token) ? encryptor.deserialize(raw_token) : raw_token
46
+ stored = encrypt ? encryptor.serialize(plaintext) : plaintext
47
+ assignments = [ "manifest_token = #{connection.quote(stored)}" ]
48
+ assignments << "manifest_token_digest = #{connection.quote(Digest::SHA256.hexdigest(plaintext))}" if encrypt
49
+
50
+ connection.execute(
51
+ "UPDATE agent_workspaces SET #{assignments.join(', ')} WHERE id = #{connection.quote(row.fetch('id'))}"
52
+ )
53
+ end
54
+ end
55
+
56
+ def encrypted?(value)
57
+ value.start_with?("{") && value.include?('"p":')
58
+ end
59
+ end