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,125 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { jest } from '@jest/globals'
6
+
7
+ const csrfFetch = jest.fn()
8
+ const alertDialog = jest.fn(() => Promise.resolve())
9
+
10
+ jest.unstable_mockModule('../../../lib/api/csrf_fetch', () => ({
11
+ default: csrfFetch,
12
+ }))
13
+ jest.unstable_mockModule('../../../lib/utils/dialog', () => ({
14
+ alertDialog,
15
+ }))
16
+
17
+ const { Application } = await import('@hotwired/stimulus')
18
+ const WriteAccessRequestController = (await import('../write_access_request_controller')).default
19
+
20
+ describe('CreativesWriteAccessRequestController', () => {
21
+ const failureMessage = '권한을 요청하지 못했습니다.'
22
+ let application
23
+ let container
24
+ let button
25
+
26
+ function markup() {
27
+ return `
28
+ <div data-controller="creatives--write-access-request" data-creatives--write-access-request-url-value="/creatives/1/request_permission" data-creatives--write-access-request-failure-message-value="${failureMessage}">
29
+ <button type="button" data-creatives--write-access-request-target="button" data-action="click->creatives--write-access-request#request">Request write access</button>
30
+ <span data-creatives--write-access-request-target="pending" hidden>Access requested</span>
31
+ </div>
32
+ `
33
+ }
34
+
35
+ beforeEach(() => {
36
+ csrfFetch.mockReset()
37
+ alertDialog.mockClear()
38
+ document.body.innerHTML = markup()
39
+ container = document.querySelector('[data-controller="creatives--write-access-request"]')
40
+ button = container.querySelector('[data-creatives--write-access-request-target="button"]')
41
+
42
+ application = Application.start()
43
+ application.register('creatives--write-access-request', WriteAccessRequestController)
44
+ })
45
+
46
+ afterEach(() => {
47
+ application.stop()
48
+ document.body.innerHTML = ''
49
+ })
50
+
51
+ test('on success, hides the button and shows the pending pill', async () => {
52
+ csrfFetch.mockResolvedValue({ ok: true })
53
+
54
+ button.click()
55
+ await new Promise((resolve) => setTimeout(resolve, 0))
56
+ await new Promise((resolve) => setTimeout(resolve, 0))
57
+
58
+ expect(csrfFetch).toHaveBeenCalledWith('/creatives/1/request_permission', { method: 'POST' })
59
+ expect(button.hidden).toBe(true)
60
+ const pending = container.querySelector('[data-creatives--write-access-request-target="pending"]')
61
+ expect(pending.hidden).toBe(false)
62
+ })
63
+
64
+ test('on non-ok response, re-enables the button and does not show the pending pill', async () => {
65
+ csrfFetch.mockResolvedValue({ ok: false })
66
+
67
+ button.click()
68
+ await new Promise((resolve) => setTimeout(resolve, 0))
69
+ await new Promise((resolve) => setTimeout(resolve, 0))
70
+
71
+ expect(button.hidden).toBe(false)
72
+ expect(button.disabled).toBe(false)
73
+ const pending = container.querySelector('[data-creatives--write-access-request-target="pending"]')
74
+ expect(pending.hidden).toBe(true)
75
+ expect(alertDialog).toHaveBeenCalledWith(failureMessage)
76
+ })
77
+
78
+ test('on network error, re-enables the button', async () => {
79
+ csrfFetch.mockRejectedValue(new Error('network down'))
80
+
81
+ button.click()
82
+ await new Promise((resolve) => setTimeout(resolve, 0))
83
+ await new Promise((resolve) => setTimeout(resolve, 0))
84
+
85
+ expect(button.hidden).toBe(false)
86
+ expect(button.disabled).toBe(false)
87
+ expect(alertDialog).toHaveBeenCalledWith(failureMessage)
88
+ })
89
+
90
+ test('does not fall back to hardcoded copy when no localized failure message is rendered', async () => {
91
+ container.removeAttribute('data-creatives--write-access-request-failure-message-value')
92
+ csrfFetch.mockResolvedValue({ ok: false })
93
+
94
+ button.click()
95
+ await new Promise((resolve) => setTimeout(resolve, 0))
96
+ await new Promise((resolve) => setTimeout(resolve, 0))
97
+
98
+ expect(button.disabled).toBe(false)
99
+ expect(alertDialog).not.toHaveBeenCalled()
100
+ })
101
+
102
+ test('on a followed redirect to the sign-in page (expired session), navigates there instead of showing pending', async () => {
103
+ // fetch() follows redirects by default, so when the session has expired,
104
+ // Authentication#request_authentication redirects to the sign-in page and
105
+ // the browser lands on a 200 OK response for the login form itself. `ok`
106
+ // is therefore true even though no permission request was ever created —
107
+ // `redirected` is what actually distinguishes this from a real success.
108
+ const redirectSpy = jest
109
+ .spyOn(WriteAccessRequestController.prototype, 'redirectToSignIn')
110
+ .mockImplementation(() => {})
111
+
112
+ csrfFetch.mockResolvedValue({ ok: true, redirected: true, url: 'http://localhost/session/new' })
113
+
114
+ button.click()
115
+ await new Promise((resolve) => setTimeout(resolve, 0))
116
+ await new Promise((resolve) => setTimeout(resolve, 0))
117
+
118
+ expect(redirectSpy).toHaveBeenCalledWith('http://localhost/session/new')
119
+ expect(button.hidden).toBe(false)
120
+ const pending = container.querySelector('[data-creatives--write-access-request-target="pending"]')
121
+ expect(pending.hidden).toBe(true)
122
+
123
+ redirectSpy.mockRestore()
124
+ })
125
+ })
@@ -1,6 +1,7 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
  import csrfFetch from '../../lib/api/csrf_fetch'
3
3
  import { confirmDialog } from '../../lib/utils/confirm_dialog'
4
+ import { removeTreeElement } from '../../modules/creative_tree_dom'
4
5
 
5
6
  export default class extends Controller {
6
7
  static targets = [
@@ -102,9 +103,13 @@ export default class extends Controller {
102
103
  detail: { creativeIds: ids.map(String) }
103
104
  }))
104
105
 
106
+ // Route through the shared helper: it drops the enclosing <creative-tree-row>
107
+ // wrapper (removing only the inner .creative-tree left an empty wrapper behind,
108
+ // which reads as "still populated") and restores the empty-state placeholder
109
+ // once the last row is gone. The destroy broadcast excludes the initiating
110
+ // user, so nothing else repairs this window's DOM.
105
111
  ids.forEach((id) => {
106
- const tree = document.getElementById(`creative-${id}`)
107
- if (tree) tree.remove()
112
+ removeTreeElement(document.getElementById(`creative-${id}`))
108
113
  })
109
114
 
110
115
  this.clearSelection()
@@ -1,19 +1,34 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
  import { renderCreativeTree, appendCreativeNodes, dispatchCreativeTreeUpdated } from '../../creatives/tree_renderer'
3
3
  import { parseEmojis } from '../../utils/emoji_parser'
4
+ import {
5
+ hideTreeEmptyState,
6
+ restoreTreeEmptyState,
7
+ PAGINATION_PENDING_ATTRIBUTE,
8
+ } from '../../modules/creative_tree_empty_state'
4
9
 
5
10
  const TREE_RETRY_DELAYS_MS = [200, 600]
6
11
 
7
12
  export default class extends Controller {
8
13
  static values = {
9
14
  url: String,
10
- emptyHtml: String,
15
+ // Neither fallback is an HTML string any more: showEmptyState() clones the
16
+ // server-rendered <template>, and the load-error fallback is a bare
17
+ // translated sentence the client wraps in a <p> itself. Reading markup back
18
+ // out of a data attribute and assigning it is an innerHTML sink as far as
19
+ // CodeQL (js/xss-through-dom) is concerned, and neither needs to be one.
20
+ errorText: String,
21
+ // Accessible name for a loading placeholder built by this controller.
22
+ // Renderers that do not provide a translated server placeholder must pass
23
+ // this value so extensions cannot expose a hardcoded label in another locale.
24
+ loadingText: String,
11
25
  }
12
26
 
13
27
  connect() {
14
28
  this.abortController = null
15
29
  this.loadingIndicator = null
16
30
  this._editing = false
31
+ this._reloadHoldCount = 0
17
32
  this._pagination = null
18
33
  this._sentinel = null
19
34
  this._sentinelObserver = null
@@ -27,10 +42,7 @@ export default class extends Controller {
27
42
  this._editing = false
28
43
  // Apply any pending sync data that was deferred while editing
29
44
  this._applyPendingSyncData()
30
- if (this._pendingRefetch) {
31
- this._pendingRefetch = false
32
- this.debouncedLoad()
33
- }
45
+ this._drainPendingReload()
34
46
  }
35
47
  document.documentElement.classList.remove('creative-alignment-ready')
36
48
  if (!this.hasCachedContent()) {
@@ -41,13 +53,7 @@ export default class extends Controller {
41
53
  this.element.addEventListener('creative-tree:updated', this.handleTreeUpdated)
42
54
  document.addEventListener('creative-editing:start', this._handleEditStart)
43
55
  document.addEventListener('creative-editing:stop', this._handleEditStop)
44
- this._handleSyncRefetch = () => {
45
- if (this._editing) {
46
- this._pendingRefetch = true
47
- return
48
- }
49
- this.debouncedLoad()
50
- }
56
+ this._handleSyncRefetch = () => this.requestReload()
51
57
  document.addEventListener('creative-sync:refetch', this._handleSyncRefetch)
52
58
  this._setupArchiveToggle()
53
59
  }
@@ -141,7 +147,57 @@ export default class extends Controller {
141
147
 
142
148
  debouncedLoad() {
143
149
  if (this._debouncedLoadTimer) clearTimeout(this._debouncedLoadTimer)
144
- this._debouncedLoadTimer = setTimeout(() => this.load(), 300)
150
+ this._debouncedLoadTimer = setTimeout(() => {
151
+ // Re-check rather than trusting the check requestReload() already made.
152
+ // Switching rows is a `creative-editing:stop` immediately followed by a
153
+ // `creative-editing:start`, so a refetch drained on the stop is still
154
+ // sitting in this debounce window when the next row opens — and load()
155
+ // replaces the whole container, which would take that row, the editor
156
+ // attached inside it and the unsaved draft out of the document. Re-pend
157
+ // instead of dropping it: the reload is still owed, just not yet safe.
158
+ if (this._editing || this._reloadHoldCount > 0) {
159
+ this._pendingRefetch = true
160
+ return
161
+ }
162
+ this.load()
163
+ }, 300)
164
+ }
165
+
166
+ // Editing-aware reload — the entry point for anything that wants the tree
167
+ // refetched in response to something that already happened elsewhere (the sync
168
+ // channel, an archive/unarchive that landed, a delete that promoted children to
169
+ // the root). load() replaces the whole container, which takes the row an open
170
+ // editor is attached to out of the document along with the unsaved draft inside
171
+ // it; those callers can land at any moment, including long after the user has
172
+ // moved on to editing a different row. So the reload waits for
173
+ // `creative-editing:stop`, which _handleEditStop drains.
174
+ //
175
+ // Call load() directly only for reloads the user just asked for and is waiting
176
+ // on (filter change, archive toggle), where re-rendering is the point.
177
+ requestReload() {
178
+ if (this._editing || this._reloadHoldCount > 0) {
179
+ this._pendingRefetch = true
180
+ return
181
+ }
182
+ this.debouncedLoad()
183
+ }
184
+
185
+ // Keep editing-aware reloads pending while an operation is between its local
186
+ // edit flush and its server response. The counter makes overlapping requests
187
+ // release independently without allowing an early reload between them.
188
+ beginReloadHold() {
189
+ this._reloadHoldCount += 1
190
+ }
191
+
192
+ endReloadHold() {
193
+ if (this._reloadHoldCount > 0) this._reloadHoldCount -= 1
194
+ this._drainPendingReload()
195
+ }
196
+
197
+ _drainPendingReload() {
198
+ if (!this._pendingRefetch || this._editing || this._reloadHoldCount > 0) return
199
+ this._pendingRefetch = false
200
+ this.debouncedLoad()
145
201
  }
146
202
 
147
203
  load() {
@@ -187,7 +243,7 @@ export default class extends Controller {
187
243
  }
188
244
  console.error(error)
189
245
  this.hideLoadingIndicator()
190
- this.showEmptyState()
246
+ this.showErrorState()
191
247
  })
192
248
  }
193
249
 
@@ -221,6 +277,9 @@ export default class extends Controller {
221
277
  this._pagination = pagination || null
222
278
  if (!this._pagination || !this._pagination.has_more) return
223
279
  if (typeof IntersectionObserver === 'undefined') return
280
+ // Deleting every rendered row does not empty the feed while further pages are
281
+ // queued, so suppress the empty-state placeholder until the last page lands.
282
+ this.element.setAttribute(PAGINATION_PENDING_ATTRIBUTE, 'true')
224
283
  this._createSentinel()
225
284
  }
226
285
 
@@ -266,6 +325,9 @@ export default class extends Controller {
266
325
  this._hideLoadMoreIndicator()
267
326
  const nodes = Array.isArray(data?.creatives) ? data.creatives : []
268
327
  if (nodes.length > 0) {
328
+ // Belt and braces alongside the pagination-pending guard: a placeholder
329
+ // that slipped through must never sit above the rows being appended.
330
+ hideTreeEmptyState(this.element)
269
331
  appendCreativeNodes(this.element, nodes)
270
332
  dispatchCreativeTreeUpdated(this.element)
271
333
  this.queueAlignmentUpdate()
@@ -276,6 +338,10 @@ export default class extends Controller {
276
338
  this._repositionSentinel()
277
339
  } else {
278
340
  this._teardownPagination()
341
+ // Last page in, and the rows that were on screen when it was requested
342
+ // may since have been deleted. Nothing is pending any more, so an empty
343
+ // container now genuinely is an empty feed.
344
+ restoreTreeEmptyState(this.element)
279
345
  }
280
346
  })
281
347
  .catch((error) => {
@@ -298,6 +364,7 @@ export default class extends Controller {
298
364
  }
299
365
 
300
366
  _teardownPagination() {
367
+ this.element.removeAttribute(PAGINATION_PENDING_ATTRIBUTE)
301
368
  if (this._loadMoreAbort) {
302
369
  this._loadMoreAbort.abort()
303
370
  this._loadMoreAbort = null
@@ -340,34 +407,76 @@ export default class extends Controller {
340
407
  }
341
408
 
342
409
  showEmptyState() {
343
- const html = this.hasEmptyHtmlValue ? this.emptyHtmlValue : ''
344
- this.element.innerHTML = html
410
+ // The placeholder comes from the server-rendered <template> rather than an
411
+ // HTML string on a data attribute: no innerHTML sink, and the button_to CSRF
412
+ // token in the "request permission" variant survives.
413
+ this.element.replaceChildren()
414
+ restoreTreeEmptyState(this.element)
415
+ this.markContentLoaded()
416
+ document.documentElement.classList.add('creative-alignment-ready')
417
+ }
418
+
419
+ // Distinct from showEmptyState(): used when the tree fetch itself fails
420
+ // (non-2xx, JSON parse failure, or a non-transient network error after
421
+ // retries are exhausted). The request never actually confirmed the tree is
422
+ // empty, so this must NOT render the Add/Import creation CTAs — a workspace
423
+ // that genuinely has creatives would otherwise look empty and invite
424
+ // duplicate creation.
425
+ showErrorState() {
426
+ this.element.replaceChildren()
427
+ if (this.hasErrorTextValue && this.errorTextValue) {
428
+ const message = document.createElement('p')
429
+ message.className = 'creative-tree-error'
430
+ message.style.textAlign = 'center'
431
+ message.textContent = this.errorTextValue
432
+ this.element.appendChild(message)
433
+ }
345
434
  this.markContentLoaded()
346
435
  document.documentElement.classList.add('creative-alignment-ready')
347
436
  }
348
437
 
349
438
  showLoadingIndicator() {
350
439
  if (!this.loadingIndicator) {
351
- const indicator = document.createElement('div')
352
- indicator.className = 'creative-tree-loading-placeholder'
353
- indicator.setAttribute('role', 'status')
354
- indicator.setAttribute('aria-live', 'polite')
355
- indicator.setAttribute('aria-label', 'Loading creatives')
356
- indicator.innerHTML = `
357
- <span class="creative-loading-indicator" aria-hidden="true">
358
- <span class="creative-loading-dot">.</span>
359
- <span class="creative-loading-dot">.</span>
360
- <span class="creative-loading-dot">.</span>
361
- </span>
362
- `
363
- this.loadingIndicator = indicator
440
+ // The first load's placeholder is already in the DOM: index.html.erb renders
441
+ // it inside #creatives so the initial paint says "loading" rather than
442
+ // flashing the empty state until Stimulus boots. Adopt that node instead of
443
+ // building a replacement — swapping it out would blank the container for a
444
+ // frame, which is the flicker this whole arrangement exists to avoid.
445
+ this.loadingIndicator =
446
+ this.element.querySelector(':scope > [data-creatives-tree-loading]') || this.buildLoadingIndicator()
364
447
  }
365
448
  this.clearLoadedState()
366
- this.element.innerHTML = ''
367
- this.element.appendChild(this.loadingIndicator)
449
+ // replaceChildren over `innerHTML = ''` + appendChild: the adopted placeholder
450
+ // is a child of the container, so wiping first would detach the very node
451
+ // being re-inserted.
452
+ this.element.replaceChildren(this.loadingIndicator)
368
453
  this.startAnimation()
369
454
  }
370
455
 
456
+ buildLoadingIndicator() {
457
+ const indicator = document.createElement('div')
458
+ indicator.className = 'creative-tree-loading-placeholder'
459
+ indicator.setAttribute('data-creatives-tree-loading', '')
460
+ indicator.setAttribute('role', 'status')
461
+ indicator.setAttribute('aria-live', 'polite')
462
+ indicator.setAttribute('aria-label', this.loadingLabel())
463
+ indicator.innerHTML = `
464
+ <span class="creative-loading-indicator" aria-hidden="true">
465
+ <span class="creative-loading-dot">.</span>
466
+ <span class="creative-loading-dot">.</span>
467
+ <span class="creative-loading-dot">.</span>
468
+ </span>
469
+ `
470
+ return indicator
471
+ }
472
+
473
+ loadingLabel() {
474
+ if (this.hasLoadingTextValue && this.loadingTextValue) return this.loadingTextValue
475
+ throw new Error(
476
+ 'creatives--tree requires a translated loadingText value when no server loading placeholder is present',
477
+ )
478
+ }
479
+
371
480
  hideLoadingIndicator() {
372
481
  this.stopAnimation()
373
482
  if (this.loadingIndicator && this.loadingIndicator.parentNode === this.element) {
@@ -0,0 +1,58 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import csrfFetch from '../../lib/api/csrf_fetch'
3
+ import { alertDialog } from '../../lib/utils/dialog'
4
+
5
+ // Powers the "Request write access" CTA in the creative tree's empty state
6
+ // (see engines/collavre/app/views/collavre/creatives/_empty_state.html.erb).
7
+ //
8
+ // There is no backend concept of a "pending request" — Collavre::Concerns::Shareable#request_permission
9
+ // just fires a one-off inbox notification to the owner, it doesn't persist request
10
+ // state anywhere. So the "pending" pill here is a purely client-side, one-time
11
+ // confirmation that the click went through: it swaps in immediately after a
12
+ // successful POST and does NOT survive a page reload (a reload has no way to
13
+ // know a request is outstanding). That's an intentional, honest scope cut —
14
+ // see the PR description for the alternatives considered.
15
+ export default class extends Controller {
16
+ static targets = ['button', 'pending']
17
+ static values = { url: String, failureMessage: String }
18
+
19
+ async request(event) {
20
+ event.preventDefault()
21
+ if (!this.hasUrlValue || !this.hasButtonTarget) return
22
+
23
+ this.buttonTarget.disabled = true
24
+ try {
25
+ const response = await csrfFetch(this.urlValue, { method: 'POST' })
26
+ if (response.redirected) {
27
+ // fetch() follows redirects by default, so an expired session sends
28
+ // this POST to the sign-in page instead of the permission endpoint —
29
+ // the final response is a 200 for the login form, not a request
30
+ // confirmation. Send the browser to the page it actually landed on
31
+ // rather than claiming the request went through.
32
+ this.redirectToSignIn(response.url)
33
+ } else if (response.ok) {
34
+ this.showPending()
35
+ } else {
36
+ this.reportFailure()
37
+ }
38
+ } catch {
39
+ this.reportFailure()
40
+ }
41
+ }
42
+
43
+ reportFailure() {
44
+ this.buttonTarget.disabled = false
45
+ if (this.hasFailureMessageValue) alertDialog(this.failureMessageValue)
46
+ }
47
+
48
+ showPending() {
49
+ this.buttonTarget.hidden = true
50
+ if (this.hasPendingTarget) {
51
+ this.pendingTarget.hidden = false
52
+ }
53
+ }
54
+
55
+ redirectToSignIn(url) {
56
+ window.location.href = url
57
+ }
58
+ }
@@ -0,0 +1,51 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import csrfFetch from "../lib/api/csrf_fetch"
3
+
4
+ export function setupNextUrl(nextUrlValue, adapters) {
5
+ const nextUrl = new URL(nextUrlValue, window.location.origin)
6
+ for (const adapter of adapters || []) nextUrl.searchParams.set(adapter, "1")
7
+ return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`
8
+ }
9
+
10
+ // The browser handles consent and a short-lived registration grant only. The
11
+ // Tauri command keeps proxy secrets in Keychain and posts them directly to the
12
+ // local Rails sidecar, so no secret is exposed to the DOM.
13
+ export default class extends Controller {
14
+ static targets = ["consent", "submit", "error"]
15
+ static values = { tokenUrl: String, nextUrl: String, unavailable: String, failed: String }
16
+
17
+ toggle() {
18
+ this.submitTarget.disabled = !this.consentTarget.checked
19
+ }
20
+
21
+ async install() {
22
+ if (!this.consentTarget.checked) {
23
+ this.submitTarget.disabled = true
24
+ return
25
+ }
26
+
27
+ this.errorTarget.hidden = true
28
+ this.submitTarget.disabled = true
29
+
30
+ try {
31
+ const invoke = window.__TAURI__?.core?.invoke
32
+ if (!invoke) throw new Error(this.unavailableValue)
33
+
34
+ const response = await csrfFetch(this.tokenUrlValue, { method: "POST" })
35
+ if (!response.ok) throw new Error(this.failedValue)
36
+ const { token } = await response.json()
37
+ const result = await invoke("desktop_proxy_complete_setup", {
38
+ registrationToken: token,
39
+ })
40
+ this.navigate(setupNextUrl(this.nextUrlValue, result.adapters))
41
+ } catch (error) {
42
+ this.errorTarget.textContent = error?.message || this.failedValue
43
+ this.errorTarget.hidden = false
44
+ this.submitTarget.disabled = !this.consentTarget.checked
45
+ }
46
+ }
47
+
48
+ navigate(url) {
49
+ window.location.assign(url)
50
+ }
51
+ }
@@ -0,0 +1,36 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["result"]
5
+ static values = { checking: String, success: String, failed: String }
6
+
7
+ async check(event) {
8
+ const button = event.currentTarget
9
+ const result = button.parentElement.querySelector("[data-gateway-check-target='result']")
10
+ button.disabled = true
11
+ result.textContent = this.checkingValue
12
+
13
+ try {
14
+ const response = await fetch(event.params.url, {
15
+ method: "POST",
16
+ headers: { "X-CSRF-Token": this.csrfToken, Accept: "application/json" }
17
+ })
18
+ const data = await response.json()
19
+ if (data.ok && data.identity_verified === false) {
20
+ result.textContent = `${this.successValue}: ${data.warning || ""}`
21
+ } else if (data.ok) {
22
+ result.textContent = `${this.successValue} (${(data.engines || []).join(", ")})`
23
+ } else {
24
+ result.textContent = `${this.failedValue}: ${data.error || ""}`
25
+ }
26
+ } catch (_error) {
27
+ result.textContent = this.failedValue
28
+ } finally {
29
+ button.disabled = false
30
+ }
31
+ }
32
+
33
+ get csrfToken() {
34
+ return document.querySelector("meta[name='csrf-token']")?.content || ""
35
+ }
36
+ }
@@ -10,6 +10,7 @@ import CreativesExpansionController from "./creatives/expansion_controller"
10
10
  import CreativesRowEditorController from "./creatives/row_editor_controller"
11
11
  import CreativesTreeController from "./creatives/tree_controller"
12
12
  import CreativesSyncController from "./creatives/sync_controller"
13
+ import CreativesWriteAccessRequestController from "./creatives/write_access_request_controller"
13
14
  import CommentsListController from "./comments/list_controller"
14
15
  import CommentsFormController from "./comments/form_controller"
15
16
  import CommentsPresenceController from "./comments/presence_controller"
@@ -17,6 +18,8 @@ import CommentsMentionMenuController from "./comments/mention_menu_controller"
17
18
  import CommentsTopicsController from "./comments/topics_controller"
18
19
  import CommentsContextsController from "./comments/contexts_controller"
19
20
  import CommentsDropTriggerController from "./comments/drop_trigger_controller"
21
+ import CommentsFeatureCardsController from "./comments/feature_cards_controller"
22
+ import CommentsPlaceholderController from "./comments/placeholder_controller"
20
23
  import CommentsPopupController from "./comments/popup_controller"
21
24
  import ClickTargetController from "./click_target_controller"
22
25
  import TabsController from "./tabs_controller"
@@ -36,6 +39,12 @@ import ImageLightboxController from "./image_lightbox_controller"
36
39
  import SearchPopupController from "./search_popup_controller"
37
40
  import LandingVideoController from "./landing_video_controller"
38
41
  import InboxBadgeController from "./inbox_badge_controller"
42
+ import LastVisitedCreativeController from "./last_visited_creative_controller"
43
+ import WorkspaceTreeController from "./workspace_tree_controller"
44
+ import GatewayCheckController from "./gateway_check_controller"
45
+ import AgentConnectionController from "./agent_connection_controller"
46
+ import AgentVendorController from "./agent_vendor_controller"
47
+ import DesktopProxySetupController from "./desktop_proxy_setup_controller"
39
48
 
40
49
  // Export all controllers
41
50
  export {
@@ -73,7 +82,13 @@ export {
73
82
  SearchPopupController,
74
83
  CommentBadgeController,
75
84
  LandingVideoController,
76
- InboxBadgeController
85
+ InboxBadgeController,
86
+ LastVisitedCreativeController,
87
+ WorkspaceTreeController,
88
+ GatewayCheckController,
89
+ AgentConnectionController,
90
+ AgentVendorController,
91
+ DesktopProxySetupController
77
92
  }
78
93
 
79
94
  // Registration function for use with a Stimulus application
@@ -88,6 +103,7 @@ export function registerControllers(application) {
88
103
  application.register("creatives--row-editor", CreativesRowEditorController)
89
104
  application.register("creatives--tree", CreativesTreeController)
90
105
  application.register("creatives--sync", CreativesSyncController)
106
+ application.register("creatives--write-access-request", CreativesWriteAccessRequestController)
91
107
  application.register("comments--list", CommentsListController)
92
108
  application.register("comments--form", CommentsFormController)
93
109
  application.register("comments--presence", CommentsPresenceController)
@@ -95,6 +111,8 @@ export function registerControllers(application) {
95
111
  application.register("comments--topics", CommentsTopicsController)
96
112
  application.register("comments--contexts", CommentsContextsController)
97
113
  application.register("comments--drop-trigger", CommentsDropTriggerController)
114
+ application.register("comments--feature-cards", CommentsFeatureCardsController)
115
+ application.register("comments--placeholder", CommentsPlaceholderController)
98
116
  application.register("comments--popup", CommentsPopupController)
99
117
  application.register("click-target", ClickTargetController)
100
118
  application.register("tabs", TabsController)
@@ -114,4 +132,10 @@ export function registerControllers(application) {
114
132
  application.register("comment-badge", CommentBadgeController)
115
133
  application.register("landing-video", LandingVideoController)
116
134
  application.register("inbox-badge", InboxBadgeController)
135
+ application.register("last-visited-creative", LastVisitedCreativeController)
136
+ application.register("workspace-tree", WorkspaceTreeController)
137
+ application.register("gateway-check", GatewayCheckController)
138
+ application.register("agent-connection", AgentConnectionController)
139
+ application.register("agent-vendor", AgentVendorController)
140
+ application.register("desktop-proxy-setup", DesktopProxySetupController)
117
141
  }