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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ # The default name for a newly created topic: the localized prefix followed
6
+ # by the next unused number ("Topic 3").
7
+ #
8
+ # Extracted from TopicsController so the topic_create MCP tool produces the
9
+ # same names as the UI. An agent fanning work out across topics names them
10
+ # far more often than a human does, and two generators would have drifted
11
+ # into two numbering series in the same sidebar.
12
+ module NextName
13
+ module_function
14
+
15
+ # Archived topics count towards the next number. Topic names are unique
16
+ # per creative whether or not the topic is archived, so the previous
17
+ # active-only scan could propose a name that cannot be saved: archive
18
+ # "Topic 1" on an otherwise unnumbered creative and the next create
19
+ # proposes "Topic 1" again and fails validation. Reachable by hand before,
20
+ # routine once an agent creates topics in a loop.
21
+ def for(creative)
22
+ prefix = I18n.t("collavre.topics.default_name_prefix")
23
+ existing_numbers = creative.topics
24
+ .where("name LIKE ?", "#{Topic.sanitize_sql_like(prefix)}%")
25
+ .pluck(:name)
26
+ .filter_map { |n|
27
+ suffix = n.delete_prefix(prefix)
28
+ suffix.match?(/\A\d+\z/) ? suffix.to_i : nil
29
+ }
30
+
31
+ "#{prefix}#{(existing_numbers.max || 0) + 1}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ # Explains a Topic.primary_agent_rejection symbol to an MCP caller.
6
+ #
7
+ # The two reasons need different advice and the difference matters: sharing
8
+ # the creative fixes a missing-permission rejection, while a Claude Channel
9
+ # session agent stays confined to its own session topic no matter what is
10
+ # shared. Telling a caller to share in that second case sends it after a fix
11
+ # that cannot work — the same distinction TopicsController draws for humans.
12
+ module PinRejection
13
+ module_function
14
+
15
+ def message(agent, rejection)
16
+ if rejection == :session_agent_outside_session_topic
17
+ "#{agent.display_name} is a session agent and can only be pinned to its own session topic."
18
+ else
19
+ "#{agent.display_name} has no feedback permission on this creative, so it cannot be pinned. " \
20
+ "Share the creative with the agent at feedback or higher first."
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ # Topic cursors follow their topic across creatives, even while a reader is
6
+ # temporarily unable to access its new location. Receipt rendering filters
7
+ # inaccessible readers, while retaining the cursor prevents a deleted source
8
+ # creative from losing that reader's history.
9
+ class ReadPointerRelocator
10
+ def initialize(topic:, target_creative:)
11
+ @topic = topic
12
+ @target_creative = target_creative
13
+ end
14
+
15
+ def call
16
+ relocate_existing_pointers
17
+ initialize_destination_pointers
18
+ end
19
+
20
+ def self.relocate(pointer, target_creative)
21
+ destination = CommentReadPointer.find_by(user: pointer.user, creative: target_creative, topic: pointer.topic)
22
+ return pointer.update_column(:creative_id, target_creative.id) unless destination
23
+
24
+ destination.update_column(:last_read_comment_id, newest_read_id(destination, pointer))
25
+ pointer.delete
26
+ end
27
+
28
+ def self.newest_read_id(destination, pointer)
29
+ [ destination.last_read_comment_id, pointer.last_read_comment_id ].compact.max
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :topic, :target_creative
35
+
36
+ def relocate_existing_pointers
37
+ CommentReadPointer.where(topic: topic).where.not(creative: target_creative).find_each do |pointer|
38
+ relocate_pointer(pointer)
39
+ end
40
+ end
41
+
42
+ def relocate_pointer(pointer)
43
+ self.class.relocate(pointer, target_creative)
44
+ end
45
+
46
+ # Existing destination cursors are the only readers that need a topic row:
47
+ # otherwise their unrelated legacy cursor becomes the moved topic fallback.
48
+ def initialize_destination_pointers
49
+ readable_user_ids = CreativeShare.readable_user_ids_from_shares(target_creative, destination_reader_ids)
50
+ rows = (readable_user_ids - destination_topic_reader_ids).map { |user_id| new_pointer_row(user_id) }
51
+ CommentReadPointer.insert_all(rows, unique_by: :index_comment_read_pointers_on_user_creative_and_topic) if rows.any?
52
+ end
53
+
54
+ def destination_reader_ids
55
+ CommentReadPointer.where(creative: target_creative).distinct.pluck(:user_id)
56
+ end
57
+
58
+ def destination_topic_reader_ids
59
+ CommentReadPointer.where(creative: target_creative, topic: topic).pluck(:user_id)
60
+ end
61
+
62
+ def new_pointer_row(user_id)
63
+ now = Time.current
64
+ {
65
+ user_id: user_id, creative_id: target_creative.id, topic_id: topic.id,
66
+ last_read_comment_id: nil, created_at: now, updated_at: now
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,18 @@
1
+ module Collavre
2
+ module Topics
3
+ module ReservedName
4
+ module_function
5
+
6
+ def reserved?(creative, name)
7
+ name == Creative::MAIN_TOPIC_NAME ||
8
+ (creative.inbox? && name == Creative::SYSTEM_TOPIC_NAME)
9
+ end
10
+
11
+ def reject!(creative, name, error_class: ArgumentError)
12
+ return unless reserved?(creative, name)
13
+
14
+ raise error_class, I18n.t("collavre.topics.reserved_name")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ # The topic payload the client merges into its cached sidebar entry, built
6
+ # outside a controller so TopicsController and the topic MCP tools cannot
7
+ # describe the same topic two different ways.
8
+ #
9
+ # The client merges rather than replaces, so a key that is sometimes omitted
10
+ # leaves stale state on screen. That is why #with_agent always carries
11
+ # :primary_agent, explicitly nil when the pin was cleared.
12
+ module Serializer
13
+ module_function
14
+
15
+ def call(topic, unread_count: nil)
16
+ data = topic.slice(:id, :name, :source_topic_id)
17
+ data[:primary_agent] = agent_json(topic.primary_agent) if topic.primary_agent
18
+ data[:agent_locked] = topic.session_id.present?
19
+ data[:archived_at] = topic.archived_at if topic.archived_at
20
+ data[:unread_count] = unread_count unless unread_count.nil?
21
+ data
22
+ end
23
+
24
+ def with_agent(topic, agent)
25
+ data = topic.slice(:id, :name, :source_topic_id)
26
+ data[:primary_agent] = agent ? agent_json(agent) : nil
27
+ data[:agent_locked] = topic.session_id.present?
28
+ data
29
+ end
30
+
31
+ # Matches ApplicationHelper#user_json's shape, but resolves the avatar
32
+ # itself. user_json reaches main_app.url_for, which needs the request that
33
+ # only the controller path has — the MCP tools broadcast this same event
34
+ # from outside one, and calling it there raises NameError. An attached
35
+ # avatar therefore resolves to a path, the way CreativesChannel already
36
+ # does for its out-of-request broadcasts; the client is same-origin, so a
37
+ # path merges into the sidebar the same as an absolute URL did.
38
+ AVATAR_SIZE = 20
39
+
40
+ def agent_json(agent)
41
+ {
42
+ id: agent.id,
43
+ name: agent.display_name,
44
+ avatar_url: avatar_url_for(agent),
45
+ default_avatar: !agent.avatar.attached? && agent.avatar_url.blank?,
46
+ initial: agent.display_name&.at(0)&.upcase || "?"
47
+ }
48
+ end
49
+
50
+ def avatar_url_for(agent)
51
+ if agent.avatar.attached?
52
+ variant = agent.avatar.variant(resize_to_fill: [ AVATAR_SIZE, AVATAR_SIZE ])
53
+ ::Rails.application.routes.url_helpers.rails_representation_path(variant, only_path: true)
54
+ elsif agent.avatar_url.present?
55
+ agent.avatar_url
56
+ else
57
+ ::ApplicationController.helpers.asset_path("default_avatar.svg")
58
+ end
59
+ end
60
+
61
+ # The compact shape the MCP tools return. Deliberately not the broadcast
62
+ # payload: an agent wants names and counts it can reason about, not the
63
+ # avatar URLs and merge-semantics keys the sidebar needs.
64
+ def for_tool(topic, message_count: nil, message_chars: nil, last_message_at: nil)
65
+ {
66
+ id: topic.id,
67
+ name: topic.name,
68
+ creative_id: topic.creative_id,
69
+ archived: topic.archived?,
70
+ main: topic.name == Creative::MAIN_TOPIC_NAME,
71
+ system: topic.creative.inbox? && topic.name == Creative::SYSTEM_TOPIC_NAME,
72
+ source_topic_id: topic.source_topic_id,
73
+ primary_agent: primary_agent_for_tool(topic),
74
+ # A session topic's pin is session identity, not a routing choice, and
75
+ # topic_update refuses to rewrite it. Say so here so an agent does not
76
+ # plan a reassignment that is going to be rejected.
77
+ agent_locked: topic.session_id.present?,
78
+ message_count: message_count,
79
+ message_chars: message_chars,
80
+ last_message_at: last_message_at&.iso8601
81
+ }.compact
82
+ end
83
+
84
+ def primary_agent_for_tool(topic)
85
+ agent = topic.primary_agent
86
+ return nil unless agent
87
+
88
+ { id: agent.id, name: agent.display_name }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ class TopicDestroy
6
+ def initialize(topic:, source_creative:)
7
+ @topic = topic
8
+ @source_creative = source_creative
9
+ end
10
+
11
+ def call
12
+ Topic.transaction do
13
+ topic.lock!
14
+ reject_changed_source!
15
+ [ topic.id, topic.name ].tap { topic.destroy! }
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :topic, :source_creative
22
+
23
+ def reject_changed_source!
24
+ return if topic.creative_id == source_creative.id
25
+
26
+ raise TopicMove::SourceChangedError, I18n.t("collavre.topics.move.source_changed")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ class TopicMove
6
+ class SourceChangedError < StandardError; end
7
+ MoveBlockedError = MoveBlocker::MoveBlockedError
8
+ ActiveTaskError = MoveBlocker::ActiveTaskError
9
+ PendingDeliveryError = MoveBlocker::PendingDeliveryError
10
+ RecurringTaskError = MoveBlocker::RecurringTaskError
11
+ TriggerLoopError = MoveBlocker::TriggerLoopError
12
+
13
+ def initialize(topic:, target_creative:)
14
+ @topic = topic
15
+ @target_creative = target_creative
16
+ @source_creative_id = topic.creative_id
17
+ end
18
+
19
+ def call(after_commit: nil)
20
+ result = Topic.transaction do
21
+ # TopicBranchService locks the source before reading its comments.
22
+ # Take the same parent-first lock order here so a branch cannot
23
+ # authorize the old creative while this transaction has already
24
+ # relocated the topic's comment rows to the new one.
25
+ topic.lock!
26
+ reject_changed_source!
27
+ lock_creative_memberships!
28
+ yield topic if block_given?
29
+ MoveBlocker.new(topic).call
30
+ clear_source_last_topic_preferences
31
+ topic.comments.update_all(creative_id: target_creative.id)
32
+ topic.comment_snapshots.update_all(creative_id: target_creative.id)
33
+ ReadPointerRelocator.new(topic: topic, target_creative: target_creative).call
34
+ topic.update!(creative: target_creative)
35
+ release_unroutable_primary_agent
36
+ end
37
+ schedule_after_commit(&after_commit) if after_commit
38
+ result
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :topic, :target_creative, :source_creative_id
44
+
45
+ def reject_changed_source!
46
+ return if topic.creative_id == source_creative_id
47
+
48
+ raise SourceChangedError, I18n.t("collavre.topics.move.source_changed")
49
+ end
50
+
51
+ def lock_creative_memberships!
52
+ Creative.where(id: [ source_creative_id, target_creative.id ].uniq).order(:id).lock.load
53
+ end
54
+
55
+ def schedule_after_commit(&callback)
56
+ ActiveRecord.after_all_transactions_commit do
57
+ Topic.transaction do
58
+ current_topic = Topic.lock.find_by(id: topic.id)
59
+ callback.call(current_topic)
60
+ end
61
+ end
62
+ end
63
+
64
+ def clear_source_last_topic_preferences
65
+ topic.user_creative_preferences_as_last_topic
66
+ .where(creative_id: source_creative_id)
67
+ .update_all("last_topic_id = NULL, last_topic_revision = last_topic_revision + 1")
68
+ end
69
+
70
+ def release_unroutable_primary_agent
71
+ agent = topic.primary_agent
72
+ rejection = agent && Topic.primary_agent_rejection(target_creative, agent, topic: topic)
73
+ return [ nil, nil ] unless rejection
74
+
75
+ topic.set_primary_agent!(nil)
76
+ [ agent, rejection ]
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ class TopicMoveEffects
6
+ def initialize(topic, source_creative, target_creative)
7
+ @topic = topic
8
+ @source_creative = source_creative
9
+ @target_creative = target_creative
10
+ end
11
+
12
+ def call(current_topic)
13
+ reset_comment_counters
14
+ unless current_topic&.creative_id == source_creative.id
15
+ TopicsChannel.broadcast_to(source_creative, { action: "deleted", topic_id: topic.id })
16
+ end
17
+ return unless current_topic&.creative_id == target_creative.id
18
+
19
+ TopicsChannel.broadcast_to(
20
+ target_creative, { action: "created", topic: Serializer.call(current_topic) }
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :topic, :source_creative, :target_creative
27
+
28
+ def reset_comment_counters
29
+ [ source_creative, target_creative ].uniq(&:id).sort_by(&:id).each do |creative|
30
+ Creative.reset_counters(creative.id, :comments)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Topics
5
+ class TopicMutation
6
+ def initialize(topic:, source_creative:)
7
+ @topic = topic
8
+ @source_creative = source_creative
9
+ end
10
+
11
+ def call
12
+ Topic.transaction do
13
+ topic.lock!
14
+ unless topic.creative_id == source_creative.id
15
+ raise TopicMove::SourceChangedError, I18n.t("collavre.topics.move.source_changed")
16
+ end
17
+
18
+ yield topic
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :topic, :source_creative
25
+ end
26
+ end
27
+ end
@@ -38,14 +38,11 @@
38
38
  rows: 8,
39
39
  style: "width: 100%; font-family: var(--font-mono, monospace); font-size: 0.85em; min-height: 8em;" %>
40
40
  <% else %>
41
- <% input_tag = definition.sensitive ? :password_field_tag : :text_field_tag %>
42
- <%= send(input_tag,
43
- "integration_setting[#{key_str}]",
44
- nil,
45
- form: bulk_form_id,
46
- placeholder: current_display,
47
- autocomplete: "off",
48
- style: "width: 100%;") %>
41
+ <% if definition.sensitive %>
42
+ <%= masked_secret_field_tag "integration_setting[#{key_str}]", nil, form: bulk_form_id, placeholder: current_display, style: "width: 100%;" %>
43
+ <% else %>
44
+ <%= text_field_tag "integration_setting[#{key_str}]", nil, form: bulk_form_id, placeholder: current_display, autocomplete: "off", style: "width: 100%;" %>
45
+ <% end %>
49
46
  <% end %>
50
47
  <div style="font-size: 0.8em; margin-top: 0.25em;">
51
48
  <span class="badge badge-source-<%= row[:source] %>" style="display: inline-block; padding: 0.1em 0.5em; font-size: 0.85em; border-radius: 3px; background: <%= source_bg %>; color: <%= source_fg %>;">
@@ -135,6 +135,13 @@
135
135
  <%= t('admin.settings.llm_request_timeout_seconds_hint') %>
136
136
  </div>
137
137
  </div>
138
+ <div style="max-width: 300px;">
139
+ <%= f.label :ai_agent_turn_deadline_seconds, t('admin.settings.ai_agent_turn_deadline_seconds') %>
140
+ <%= f.number_field :ai_agent_turn_deadline_seconds, value: @ai_agent_turn_deadline_seconds, min: 60, style: "width: 100%;" %>
141
+ <div class="help-text" style="font-size: 0.85em; color: var(--color-text-muted); margin-top: 0.25em;">
142
+ <%= t('admin.settings.ai_agent_turn_deadline_seconds_hint') %>
143
+ </div>
144
+ </div>
138
145
  </div>
139
146
 
140
147
  <div style="margin-top: 2em; border-top: 1px solid var(--color-border); padding-top: 1.5em;">
@@ -0,0 +1,77 @@
1
+ <h1 class="no-top-margin"><%= t("collavre.agent_connections.title", agent: @agent.name) %></h1>
2
+
3
+ <div class="alert alert-info">
4
+ <strong><%= @agent.agent_gateway.name %></strong>
5
+ · <%= t("collavre.agent_gateways.workspace_mode.#{@agent.agent_gateway.workspace_mode}") %>
6
+ · <%= t("collavre.agent_connections.credential_axis") %> <code><%= @workspace.proxy_credential_id %></code>
7
+ · <%= t("collavre.agent_connections.workspace_axis") %> <code><%= @workspace.proxy_workspace_id %></code>
8
+ <p class="text-muted" style="margin-bottom: 0;">
9
+ <%= t("collavre.agent_connections.axis_help.#{@agent.agent_gateway.workspace_mode}") %>
10
+ </p>
11
+ </div>
12
+
13
+ <div data-controller="agent-connection"
14
+ data-agent-connection-status-url-value="<%= collavre.agent_connection_status_path(@agent) %>"
15
+ data-agent-connection-session-url-value="<%= collavre.agent_connection_auth_sessions_path(@agent, engine: '__ENGINE__') %>"
16
+ data-agent-connection-session-detail-url-value="<%= collavre.agent_connection_auth_session_path(@agent, engine: '__ENGINE__', session_id: '__SESSION__') %>"
17
+ data-agent-connection-sync-url-value="<%= collavre.agent_connection_provision_sync_path(@agent) %>"
18
+ data-agent-connection-approve-url-value="<%= collavre.agent_connection_provision_approve_path(@agent, type: '__TYPE__', name: '__NAME__') %>"
19
+ data-agent-connection-delete-url-value="<%= collavre.agent_connection_provision_delete_path(@agent, type: '__TYPE__', name: '__NAME__') %>"
20
+ data-agent-connection-loading-value="<%= t('collavre.agent_connections.loading') %>"
21
+ data-agent-connection-login-value="<%= t('collavre.agent_connections.login') %>"
22
+ data-agent-connection-submit-value="<%= t('collavre.agent_connections.submit') %>"
23
+ data-agent-connection-base-url-label-value="<%= t('collavre.agent_connections.base_url.label') %>"
24
+ data-agent-connection-base-url-help-value="<%= t('collavre.agent_connections.base_url.help') %>"
25
+ data-agent-connection-cancel-value="<%= t('common.cancel') %>"
26
+ data-agent-connection-open-url-value="<%= t('collavre.agent_connections.open_url') %>"
27
+ data-agent-connection-approve-value="<%= t('collavre.agent_connections.approve') %>"
28
+ data-agent-connection-revoke-value="<%= t('collavre.agent_connections.revoke') %>"
29
+ data-agent-connection-error-value="<%= t('collavre.agent_connections.error') %>"
30
+ data-agent-connection-manifest-unregistered-value="<%= t('collavre.agent_connections.manifest_unregistered') %>"
31
+ data-agent-connection-last-error-value="<%= t('collavre.agent_connections.last_error') %>"
32
+ data-agent-connection-status-labels-value="<%= t('collavre.agent_connections.statuses').to_json %>"
33
+ data-agent-connection-item-type-labels-value="<%= t('collavre.agent_connections.item_types').to_json %>">
34
+ <section class="form-section">
35
+ <div class="flex-row-center-between">
36
+ <h2><%= t("collavre.agent_connections.engines") %></h2>
37
+ <button type="button" class="btn btn-sm btn-secondary" data-action="agent-connection#refresh"><%= t("collavre.agent_connections.refresh") %></button>
38
+ </div>
39
+ <div data-agent-connection-target="error" class="alert alert-danger" hidden></div>
40
+ <div data-agent-connection-target="engines"><%= t("collavre.agent_connections.loading") %></div>
41
+ <div data-agent-connection-target="session"></div>
42
+ </section>
43
+
44
+ <section class="form-section" style="margin-top: 1.5rem;">
45
+ <div class="flex-row-center-between">
46
+ <div>
47
+ <h2><%= t("collavre.agent_connections.provisioning") %></h2>
48
+ <p class="text-muted"><%= t("collavre.agent_connections.provisioning_help") %></p>
49
+ </div>
50
+ <button type="button" class="btn btn-sm btn-primary" data-action="agent-connection#sync"><%= t("collavre.agent_connections.sync") %></button>
51
+ </div>
52
+ <div data-agent-connection-target="manifest"></div>
53
+ <table class="settings-table" style="width:100%">
54
+ <thead><tr><th><%= t("collavre.agent_connections.item") %></th><th><%= t("collavre.agent_connections.kind") %></th><th><%= t("collavre.agent_connections.state") %></th><th><%= t("collavre.agent_connections.actions") %></th></tr></thead>
55
+ <tbody data-agent-connection-target="provision">
56
+ <tr><td><strong>collavre</strong></td><td><%= t("collavre.agent_connections.item_types.skill") %></td><td><%= t("collavre.agent_connections.loading") %></td><td></td></tr>
57
+ <tr><td><strong>collavre</strong></td><td><%= t("collavre.agent_connections.item_types.config") %></td><td><%= t("collavre.agent_connections.loading") %></td><td></td></tr>
58
+ </tbody>
59
+ </table>
60
+ </section>
61
+ </div>
62
+
63
+ <section class="form-section" style="margin-top: 1.5rem;">
64
+ <h2><%= t("collavre.agent_connections.meta_skills") %></h2>
65
+ <p class="text-muted"><%= t("collavre.agent_connections.meta_skills_help") %></p>
66
+ <% if @agent.tools.present? %>
67
+ <ul><% @agent.tools.each do |tool| %><li><code><%= tool %></code></li><% end %></ul>
68
+ <% else %>
69
+ <p class="text-muted"><%= t("collavre.users.edit_ai.no_tools_found") %></p>
70
+ <% end %>
71
+ </section>
72
+
73
+ <div class="flex-row-center-between" style="margin-top: 1.5rem;">
74
+ <%= link_to t("collavre.agent_connections.back"), collavre.user_path(@agent), class: "btn btn-secondary" %>
75
+ <%= button_to t("collavre.agent_connections.rotate_tokens"), collavre.agent_connection_rotate_tokens_path(@agent), method: :post,
76
+ class: "btn btn-danger", form: { data: { turbo_confirm: t("collavre.agent_connections.rotate_confirm") } } %>
77
+ </div>
@@ -0,0 +1,71 @@
1
+ <%= form_with model: agent_gateway, class: "stacked-form" do |form| %>
2
+ <% if agent_gateway.errors.any? %>
3
+ <div class="alert alert-danger" role="alert">
4
+ <ul class="mb-0 ps-3">
5
+ <% agent_gateway.errors.full_messages.each do |message| %>
6
+ <li><%= message %></li>
7
+ <% end %>
8
+ </ul>
9
+ </div>
10
+ <% end %>
11
+
12
+ <div class="form-group">
13
+ <%= form.label :name, t("collavre.agent_gateways.name") %>
14
+ <%= form.text_field :name, required: true, class: "stacked-form-control", placeholder: t("collavre.agent_gateways.name_placeholder") %>
15
+ </div>
16
+ <div class="form-group">
17
+ <%= form.label :base_url, t("collavre.agent_gateways.base_url") %>
18
+ <%= form.url_field :base_url, required: true, class: "stacked-form-control", placeholder: "https://proxy.example.com" %>
19
+ <small class="form-text text-muted">
20
+ <%= t(Current.user.system_admin? ? "collavre.agent_gateways.base_url_help_admin" : "collavre.agent_gateways.base_url_help") %>
21
+ </small>
22
+ </div>
23
+
24
+ <% secret_placeholder = agent_gateway.persisted? ? t("collavre.agent_gateways.secret_unchanged") : nil %>
25
+ <div class="form-group">
26
+ <%= form.label :admin_key, t("collavre.agent_gateways.admin_key") %>
27
+ <%= masked_secret_field form, :admin_key, value: nil, required: agent_gateway.new_record?, class: "stacked-form-control", placeholder: secret_placeholder %>
28
+ <small class="form-text text-muted"><%= t("collavre.agent_gateways.admin_key_help") %></small>
29
+ </div>
30
+ <div class="form-group">
31
+ <%= form.label :completion_key, t("collavre.agent_gateways.completion_key") %>
32
+ <%= masked_secret_field form, :completion_key, value: nil, class: "stacked-form-control", placeholder: secret_placeholder %>
33
+ <small class="form-text text-muted"><%= t("collavre.agent_gateways.completion_key_help") %></small>
34
+ <% if agent_gateway.persisted? && @has_stored_completion_key %>
35
+ <div class="form-check mt-2">
36
+ <%= check_box_tag "agent_gateway[clear_completion_key]", "1", @clear_completion_key, id: "agent_gateway_clear_completion_key", class: "form-check-input" %>
37
+ <%= label_tag "agent_gateway_clear_completion_key", t("collavre.agent_gateways.clear_completion_key_label"), class: "form-check-label" %>
38
+ </div>
39
+ <small class="form-text text-muted"><%= t("collavre.agent_gateways.clear_completion_key_help") %></small>
40
+ <% end %>
41
+ </div>
42
+ <div class="form-group">
43
+ <%= form.label :identity_secret, t("collavre.agent_gateways.identity_secret") %>
44
+ <%= masked_secret_field form, :identity_secret, value: nil, class: "stacked-form-control", placeholder: secret_placeholder %>
45
+ <small class="form-text text-muted"><%= t("collavre.agent_gateways.identity_secret_help") %></small>
46
+ </div>
47
+ <div class="form-group">
48
+ <%= form.label :tenant_id, t("collavre.agent_gateways.tenant_id") %>
49
+ <%= form.text_field :tenant_id, required: true, class: "stacked-form-control", value: agent_gateway.tenant_id.presence || "collavre" %>
50
+ <small class="form-text text-muted"><%= t("collavre.agent_gateways.tenant_id_help") %></small>
51
+ </div>
52
+
53
+ <fieldset class="form-group">
54
+ <legend class="form-label"><%= t("collavre.agent_gateways.workspace_mode.label") %></legend>
55
+ <% Collavre::AgentGateway.workspace_modes.each_key do |mode| %>
56
+ <div class="form-check mb-2">
57
+ <%= form.radio_button :workspace_mode, mode, checked: agent_gateway.workspace_mode == mode, class: "form-check-input" %>
58
+ <%= form.label "workspace_mode_#{mode}", t("collavre.agent_gateways.workspace_mode.#{mode}"), class: "form-check-label" %>
59
+ <small class="form-text text-muted"><%= t("collavre.agent_gateways.workspace_mode.#{mode}_help") %></small>
60
+ </div>
61
+ <% end %>
62
+ </fieldset>
63
+
64
+ <div class="form-group form-check">
65
+ <%= form.check_box :active, class: "form-check-input" %>
66
+ <%= form.label :active, t("collavre.agent_gateways.active"), class: "form-check-label" %>
67
+ </div>
68
+
69
+ <%= form.submit t("common.save"), class: "primary-action-button" %>
70
+ <%= link_to t("common.cancel"), collavre.agent_gateways_path, class: "btn btn-secondary" %>
71
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <h1 class="no-top-margin"><%= t("collavre.agent_gateways.edit_title") %></h1>
2
+ <%= render "form", agent_gateway: @agent_gateway %>