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,221 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { jest } from '@jest/globals'
6
+ import CommentsListController from '../list_controller'
7
+
8
+ const streamEvent = ({ action = 'append', target = 'comments-list', html = '' } = {}) => {
9
+ const template = document.createElement('template')
10
+ template.innerHTML = html
11
+ return {
12
+ preventDefault: jest.fn(),
13
+ target: { action, target, templateContent: template.content },
14
+ }
15
+ }
16
+
17
+ const comment = (topicId, commentId = 9) =>
18
+ `<div id="comment_${commentId}" class="comment-item" data-comment-id="${commentId}" data-topic-id="${topicId}">hello</div>`
19
+
20
+ // currentTopicId "" is the All Messages view. archivedIds stands in for the
21
+ // topics controller's archived_topics, which is what the real lookup reads.
22
+ const buildController = ({ currentTopicId = '', archivedIds = [], withPopup = true, searchQuery = null } = {}) => {
23
+ const controller = Object.create(CommentsListController.prototype)
24
+ controller.manualSearchQuery = searchQuery
25
+ controller.currentTopicId = currentTopicId
26
+ controller.allNewerLoaded = true
27
+ controller.markCommentsRead = jest.fn()
28
+
29
+ // popupController is a prototype getter reaching into this.application; an
30
+ // own property shadows it without standing up a Stimulus application.
31
+ Object.defineProperty(controller, 'popupController', {
32
+ value: withPopup
33
+ ? {
34
+ topicsController: {
35
+ isArchivedTopic: (id) => archivedIds.some((a) => String(a) === String(id)),
36
+ },
37
+ }
38
+ : null,
39
+ })
40
+
41
+ return controller
42
+ }
43
+
44
+ describe('CommentsListController archived streams in All Messages', () => {
45
+ let badgeEvents
46
+ const onBadge = (e) => badgeEvents.push(e.detail)
47
+
48
+ beforeEach(() => {
49
+ badgeEvents = []
50
+ window.addEventListener('comments--topics:new-message', onBadge)
51
+ })
52
+
53
+ afterEach(() => {
54
+ window.removeEventListener('comments--topics:new-message', onBadge)
55
+ })
56
+
57
+ // CommentsController#index excludes archived-topic comments from All Messages,
58
+ // so appending one live would show a message that disappears on reload.
59
+ test('blocks an archived-topic message and badges the topic instead', () => {
60
+ const controller = buildController({ archivedIds: [7] })
61
+ const event = streamEvent({ html: comment(7) })
62
+
63
+ controller.handleStreamRender(event)
64
+
65
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
66
+ expect(badgeEvents).toEqual([{ topicId: '7' }])
67
+ })
68
+
69
+ test('lets a live topic message through', () => {
70
+ const controller = buildController({ archivedIds: [7] })
71
+ const event = streamEvent({ html: comment(2) })
72
+
73
+ controller.handleStreamRender(event)
74
+
75
+ expect(event.preventDefault).not.toHaveBeenCalled()
76
+ expect(badgeEvents).toEqual([])
77
+ expect(controller.markCommentsRead).toHaveBeenCalledTimes(1)
78
+ })
79
+
80
+ test('extends the rendered All Messages watermark for a visible live append', () => {
81
+ const controller = buildController()
82
+ controller.renderedAllTopicIds = ['2']
83
+ controller.renderedAllTopicWatermarks = { 2: 8 }
84
+
85
+ controller.handleStreamRender(streamEvent({ html: comment(2, 9) }))
86
+
87
+ expect(controller.renderedAllTopicWatermarks).toEqual({ 2: 9 })
88
+ expect(controller.markCommentsRead).toHaveBeenCalledTimes(1)
89
+ })
90
+
91
+ test('starts a watermark for an initially active topic omitted from the list window', () => {
92
+ const controller = buildController()
93
+ controller.renderedAllTopicIds = ['2', '3']
94
+ controller.renderedAllTopicWatermarks = { 2: 8 }
95
+
96
+ controller.handleStreamRender(streamEvent({ html: comment(3, 9) }))
97
+
98
+ expect(controller.renderedAllTopicWatermarks).toEqual({ 2: 8, 3: 9 })
99
+ expect(controller.markCommentsRead).toHaveBeenCalledTimes(1)
100
+ })
101
+
102
+ test('does not add a topic absent from the rendered All Messages snapshot', () => {
103
+ const controller = buildController()
104
+ controller.renderedAllTopicIds = ['2']
105
+ controller.renderedAllTopicWatermarks = { 2: 8 }
106
+
107
+ controller.handleStreamRender(streamEvent({ html: comment(3, 9) }))
108
+
109
+ expect(controller.renderedAllTopicWatermarks).toEqual({ 2: 8 })
110
+ })
111
+
112
+ test('blocks a restored topic absent from the rendered All Messages snapshot', () => {
113
+ const controller = buildController({ archivedIds: [] })
114
+ controller.renderedAllTopicIds = ['2']
115
+ const event = streamEvent({ html: comment(3, 9) })
116
+
117
+ controller.handleStreamRender(event)
118
+
119
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
120
+ expect(badgeEvents).toEqual([{ topicId: '3' }])
121
+ expect(controller.markCommentsRead).not.toHaveBeenCalled()
122
+ })
123
+
124
+ test('does not mark a foreign message read', () => {
125
+ const controller = buildController({ currentTopicId: '2' })
126
+ const event = streamEvent({ html: comment(5) })
127
+
128
+ controller.handleStreamRender(event)
129
+
130
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
131
+ expect(controller.markCommentsRead).not.toHaveBeenCalled()
132
+ })
133
+
134
+ test('lets a topic-less comment through without consulting the topics controller', () => {
135
+ const controller = buildController({ archivedIds: [7], withPopup: false })
136
+ const event = streamEvent({ html: comment('') })
137
+
138
+ controller.handleStreamRender(event)
139
+
140
+ expect(event.preventDefault).not.toHaveBeenCalled()
141
+ expect(badgeEvents).toEqual([])
142
+ })
143
+
144
+ // The archived section may not have loaded yet on a docked chat; a missing
145
+ // topics controller must not break live appends.
146
+ test('appends normally when the topics controller is unavailable', () => {
147
+ const controller = buildController({ archivedIds: [7], withPopup: false })
148
+ const event = streamEvent({ html: comment(7) })
149
+
150
+ controller.handleStreamRender(event)
151
+
152
+ expect(event.preventDefault).not.toHaveBeenCalled()
153
+ })
154
+
155
+ test('still blocks a foreign topic when viewing a specific topic', () => {
156
+ const controller = buildController({ currentTopicId: '2', archivedIds: [] })
157
+ const event = streamEvent({ html: comment(5) })
158
+
159
+ controller.handleStreamRender(event)
160
+
161
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
162
+ expect(badgeEvents).toEqual([{ topicId: '5' }])
163
+ })
164
+
165
+ test('appends a message for the archived topic the user is actually viewing', () => {
166
+ const controller = buildController({ currentTopicId: '7', archivedIds: [7] })
167
+ const event = streamEvent({ html: comment(7) })
168
+
169
+ controller.handleStreamRender(event)
170
+
171
+ expect(event.preventDefault).not.toHaveBeenCalled()
172
+ expect(badgeEvents).toEqual([])
173
+ })
174
+
175
+ // An active search blocks every append, so the badge is the only thing left
176
+ // that can report traffic in a topic the user cannot see. It has to survive
177
+ // the search block, not be swallowed by it.
178
+ describe('while a search filter is active', () => {
179
+ test('badges an archived topic even though the append is blocked', () => {
180
+ const controller = buildController({ archivedIds: [7], searchQuery: 'deploy' })
181
+ const event = streamEvent({ html: comment(7) })
182
+
183
+ controller.handleStreamRender(event)
184
+
185
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
186
+ expect(badgeEvents).toEqual([{ topicId: '7' }])
187
+ })
188
+
189
+ test('badges a foreign topic while searching inside a specific topic', () => {
190
+ const controller = buildController({ currentTopicId: '2', searchQuery: 'deploy' })
191
+ const event = streamEvent({ html: comment(5) })
192
+
193
+ controller.handleStreamRender(event)
194
+
195
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
196
+ expect(badgeEvents).toEqual([{ topicId: '5' }])
197
+ })
198
+
199
+ // The search block still owns everything the topic routing lets past: the
200
+ // result set is a server-side match and a live append carries no verdict.
201
+ test('still blocks a message for the topic in view, without badging it', () => {
202
+ const controller = buildController({ currentTopicId: '2', searchQuery: 'deploy' })
203
+ const event = streamEvent({ html: comment(2) })
204
+
205
+ controller.handleStreamRender(event)
206
+
207
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
208
+ expect(badgeEvents).toEqual([])
209
+ })
210
+
211
+ test('still blocks a live-topic message in All Messages, without badging it', () => {
212
+ const controller = buildController({ archivedIds: [7], searchQuery: 'deploy' })
213
+ const event = streamEvent({ html: comment(2) })
214
+
215
+ controller.handleStreamRender(event)
216
+
217
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
218
+ expect(badgeEvents).toEqual([])
219
+ })
220
+ })
221
+ })
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { jest } from '@jest/globals'
6
+ import CommentsListController from '../list_controller'
7
+
8
+ // Builds the shape handleStreamRender reads off a turbo:before-stream-render
9
+ // event: the <turbo-stream> element plus a preventDefault spy.
10
+ const streamEvent = ({ action = 'append', target = 'comments-list', html = '' } = {}) => {
11
+ const template = document.createElement('template')
12
+ template.innerHTML = html
13
+ return {
14
+ preventDefault: jest.fn(),
15
+ target: { action, target, templateContent: template.content },
16
+ }
17
+ }
18
+
19
+ const buildController = ({ manualSearchQuery = null, currentTopicId = null } = {}) => {
20
+ const controller = Object.create(CommentsListController.prototype)
21
+ controller.manualSearchQuery = manualSearchQuery
22
+ controller.currentTopicId = currentTopicId
23
+ controller.allNewerLoaded = true
24
+ return controller
25
+ }
26
+
27
+ describe('CommentsListController live appends during search', () => {
28
+ test('blocks a live comment while a search is active', () => {
29
+ const controller = buildController({ manualSearchQuery: 'invoice' })
30
+ const event = streamEvent({
31
+ html: '<div id="comment_9" class="comment-item" data-topic-id="">unrelated chatter</div>',
32
+ })
33
+
34
+ controller.handleStreamRender(event)
35
+
36
+ // The server decides what matches; an append carries no such verdict, so it
37
+ // must not land in the result set (nor clear #no-search-results with it).
38
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
39
+ })
40
+
41
+ test('blocks it even when the message would match the query', () => {
42
+ // Deliberate: matching client-side would have to reimplement the
43
+ // controller's multi-word LOWER(content) LIKE filter over raw markdown.
44
+ // The snapshot stays consistent instead of half-refreshed.
45
+ const controller = buildController({ manualSearchQuery: 'invoice' })
46
+ const event = streamEvent({
47
+ html: '<div id="comment_9" class="comment-item" data-topic-id="">the invoice is ready</div>',
48
+ })
49
+
50
+ controller.handleStreamRender(event)
51
+
52
+ expect(event.preventDefault).toHaveBeenCalledTimes(1)
53
+ })
54
+
55
+ test('lets live comments through when no search is active', () => {
56
+ const controller = buildController({ manualSearchQuery: null })
57
+ const event = streamEvent({
58
+ html: '<div id="comment_9" class="comment-item" data-topic-id="">hello</div>',
59
+ })
60
+
61
+ controller.handleStreamRender(event)
62
+
63
+ expect(event.preventDefault).not.toHaveBeenCalled()
64
+ })
65
+
66
+ test('still applies edits and deletions to comments inside the results', () => {
67
+ // Only appends are filtered. A replace/remove targets a comment already in
68
+ // the result set, so suppressing it would leave stale text on screen.
69
+ const controller = buildController({ manualSearchQuery: 'invoice' })
70
+
71
+ const replace = streamEvent({ action: 'replace' })
72
+ const remove = streamEvent({ action: 'remove' })
73
+ controller.handleStreamRender(replace)
74
+ controller.handleStreamRender(remove)
75
+
76
+ expect(replace.preventDefault).not.toHaveBeenCalled()
77
+ expect(remove.preventDefault).not.toHaveBeenCalled()
78
+ })
79
+
80
+ test('ignores streams aimed at another target', () => {
81
+ const controller = buildController({ manualSearchQuery: 'invoice' })
82
+ const event = streamEvent({ target: 'topic-list' })
83
+
84
+ controller.handleStreamRender(event)
85
+
86
+ expect(event.preventDefault).not.toHaveBeenCalled()
87
+ })
88
+
89
+ test('resetState clears the filter so live comments resume', () => {
90
+ const controller = buildController({ manualSearchQuery: 'invoice' })
91
+ controller.selection = new Set()
92
+ controller.notifySelectionChange = jest.fn()
93
+
94
+ controller.resetState()
95
+
96
+ const event = streamEvent({
97
+ html: '<div id="comment_9" class="comment-item" data-topic-id="">hello</div>',
98
+ })
99
+ controller.handleStreamRender(event)
100
+
101
+ expect(controller.manualSearchQuery).toBeNull()
102
+ expect(event.preventDefault).not.toHaveBeenCalled()
103
+ })
104
+ })
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { Application } from '@hotwired/stimulus'
6
+ import PlaceholderController from '../placeholder_controller'
7
+
8
+ describe('PlaceholderController', () => {
9
+ let application
10
+ let list
11
+
12
+ // The two empty-list states rendered by collavre/comments/_list.html.erb.
13
+ const PLACEHOLDERS = [
14
+ { id: 'no-comments', label: 'discovery cards' },
15
+ { id: 'no-search-results', label: 'no-search-results notice' },
16
+ ]
17
+
18
+ const mount = (id) => {
19
+ list = document.createElement('div')
20
+ list.id = 'comments-list'
21
+ list.innerHTML = `<div id="${id}" class="comments-placeholder" data-controller="comments--placeholder"><p>empty</p></div>`
22
+ document.body.appendChild(list)
23
+ }
24
+
25
+ const appendComment = () => {
26
+ const comment = document.createElement('div')
27
+ comment.className = 'comment-item'
28
+ list.appendChild(comment)
29
+ return new Promise((r) => setTimeout(r, 0))
30
+ }
31
+
32
+ beforeEach(() => {
33
+ application = Application.start()
34
+ application.register('comments--placeholder', PlaceholderController)
35
+ })
36
+
37
+ afterEach(() => {
38
+ application.stop()
39
+ document.body.innerHTML = ''
40
+ })
41
+
42
+ PLACEHOLDERS.forEach(({ id, label }) => {
43
+ test(`removes the ${label} when a comment is appended by a Turbo Stream`, async () => {
44
+ // Comment#broadcast_create appends straight into #comments-list; nothing
45
+ // on that path calls comments--form#removePlaceholder.
46
+ mount(id)
47
+ await new Promise((r) => setTimeout(r, 0))
48
+
49
+ await appendComment()
50
+
51
+ expect(list.querySelector(`#${id}`)).toBeNull()
52
+ })
53
+ })
54
+
55
+ test('stays put when a non-comment node is appended to the list', async () => {
56
+ mount('no-comments')
57
+ await new Promise((r) => setTimeout(r, 0))
58
+
59
+ list.appendChild(document.createElement('div'))
60
+ await new Promise((r) => setTimeout(r, 0))
61
+
62
+ expect(list.querySelector('#no-comments')).not.toBeNull()
63
+ })
64
+
65
+ test('stops observing once disconnected', async () => {
66
+ mount('no-comments')
67
+ await new Promise((r) => setTimeout(r, 0))
68
+
69
+ const placeholder = list.querySelector('#no-comments')
70
+ placeholder.removeAttribute('data-controller')
71
+ await new Promise((r) => setTimeout(r, 0))
72
+
73
+ await appendComment()
74
+
75
+ // Detached from its controller, the element is left alone — proving the
76
+ // observer is torn down rather than leaking past disconnect().
77
+ expect(list.querySelector('#no-comments')).not.toBeNull()
78
+ })
79
+ })