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
@@ -61,9 +61,7 @@ nav #search {
61
61
  .progress-filter-btn {
62
62
  border-radius: var(--radius-3);
63
63
  border: none;
64
- background: transparent;
65
64
  padding: var(--space-1) var(--space-2);
66
- transition: background 0.2s var(--ease-out-2);
67
65
  }
68
66
 
69
67
  .progress-filter-btn:first-child {
@@ -79,11 +77,74 @@ nav #search {
79
77
  }
80
78
 
81
79
  .progress-filter-btn.active {
82
- background: var(--surface-hover);
83
80
  color: var(--text-primary);
84
81
  font-weight: var(--weight-5);
85
82
  }
86
83
 
84
+ /*
85
+ * Nav pills that toggle their selected surface in place
86
+ * -----------------------------------------------------
87
+ * The chat filter and the search trigger flip `.active` on a live page during
88
+ * turbo-frame navigation, so their surface changes where it sits instead of
89
+ * arriving already painted in a fresh document. Animating `background` there
90
+ * re-rasterises the pill's rounded corners on every frame of the transition,
91
+ * and mobile GPU rasterisers have been observed to mis-composite a corner arc
92
+ * under that load: the bottom-left arc is replaced by its own chord and the
93
+ * clipped sliver is drawn back on top, which reads as a folded corner.
94
+ *
95
+ * Put the surface on an overlay pseudo-element and cross-fade it with
96
+ * `opacity`, which the compositor animates without repainting. The rounded
97
+ * rect is rasterised once and only its alpha moves.
98
+ *
99
+ * `z-index: -1` keeps the overlay above the button's own background box and
100
+ * below its text; that needs the button to be a stacking context, hence
101
+ * `position: relative` + `z-index: 0`.
102
+ */
103
+ .progress-filter-btn,
104
+ .search-popup-trigger {
105
+ position: relative;
106
+ z-index: 0;
107
+ transition: color 0.2s var(--ease-out-2);
108
+ }
109
+
110
+ .progress-filter-btn::before,
111
+ .search-popup-trigger::before {
112
+ content: "";
113
+ position: absolute;
114
+ inset: 0;
115
+ z-index: -1;
116
+ border-radius: inherit;
117
+ background: var(--surface-hover);
118
+ opacity: 0;
119
+ transition: opacity 0.2s var(--ease-out-2);
120
+ pointer-events: none;
121
+ will-change: opacity;
122
+ }
123
+
124
+ .progress-filter-btn:hover::before,
125
+ .progress-filter-btn:active::before,
126
+ .progress-filter-btn.active::before,
127
+ .search-popup-trigger:hover::before,
128
+ .search-popup-trigger:active::before,
129
+ .search-popup-trigger.active::before {
130
+ opacity: 1;
131
+ }
132
+
133
+ /*
134
+ * The button itself has to stay transparent in every state, otherwise the
135
+ * `nav a/button` surfaces above would stack on top of the overlay.
136
+ */
137
+ .progress-filter-btn,
138
+ .progress-filter-btn:hover,
139
+ .progress-filter-btn:active,
140
+ .progress-filter-btn.active,
141
+ .search-popup-trigger,
142
+ .search-popup-trigger:hover,
143
+ .search-popup-trigger:active,
144
+ .search-popup-trigger.active {
145
+ background: transparent;
146
+ }
147
+
87
148
  /* ChatGPT-style popup menu overrides */
88
149
  nav .popup-menu-wrapper .popup-menu {
89
150
  border: 1px solid var(--border-color);
@@ -16,9 +16,12 @@
16
16
  scroll-behavior: smooth;
17
17
  }
18
18
 
19
- .landing-page nav {
20
- display: none; /* hide app navigation */
21
- }
19
+ /* This file is linked only from layouts/collavre/landing.html.erb, which renders
20
+ `<main><%= yield %></main>` and no app navigation — that lives in the host's
21
+ application layout, which never loads this stylesheet. So a blanket
22
+ `.landing-page nav { display: none }` could only ever hide a <nav> authored by
23
+ a landing-layout view (the feature-guide breadcrumbs), which is the opposite
24
+ of what it was for. Removed rather than narrowed. */
22
25
 
23
26
  .landing {
24
27
  font-family: var(--font-sans);
@@ -336,6 +339,7 @@
336
339
  margin-bottom: var(--space-3);
337
340
  }
338
341
 
342
+ .landing-feature-card h2,
339
343
  .landing-feature-card h3 { /* L3 */
340
344
  font-size: var(--text-3);
341
345
  font-weight: var(--weight-6);
@@ -597,3 +601,104 @@ body.dark-mode .landing-shot-dark { display: block; }
597
601
  .landing-hero-sub { animation-delay: 0.1s; }
598
602
  .landing-hero-brand { animation-delay: 0.2s; }
599
603
  .landing-hero-actions { animation-delay: 0.3s; }
604
+
605
+ /* ─── Feature Guides (/features, /features/:key) ─────────────────────────────
606
+ The public long-form counterpart to the empty-chat feature cards. Shares the
607
+ landing layout and tokens so a guide reached from a shared link reads as the
608
+ same product surface as the landing page it sits beside. */
609
+ .landing-features-more {
610
+ margin-top: var(--space-6);
611
+ text-align: center;
612
+ }
613
+
614
+ .feature-guide-breadcrumb {
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: center;
618
+ gap: var(--space-2);
619
+ font-size: var(--text-1);
620
+ color: var(--text-muted);
621
+ margin-bottom: var(--space-6);
622
+ }
623
+
624
+ .feature-guide-breadcrumb a {
625
+ color: var(--text-muted);
626
+ text-decoration: none;
627
+ }
628
+
629
+ .feature-guide-breadcrumb a:hover {
630
+ color: var(--color-active);
631
+ text-decoration: underline;
632
+ }
633
+
634
+ /* The whole card is the link, so strip the anchor's own presentation and let
635
+ .landing-feature-card keep supplying it. */
636
+ .feature-guide-link-card {
637
+ display: block;
638
+ color: inherit;
639
+ text-decoration: none;
640
+ }
641
+
642
+ .feature-guide-more {
643
+ display: inline-block;
644
+ margin-top: var(--space-3);
645
+ font-size: var(--text-1);
646
+ font-weight: var(--weight-6);
647
+ color: var(--color-active);
648
+ }
649
+
650
+ /* A guide is read top to bottom rather than scanned, so it drops the centered
651
+ landing rhythm for a single measured column. */
652
+ .feature-guide-container {
653
+ max-width: 720px;
654
+ }
655
+
656
+ .feature-guide-header {
657
+ text-align: center;
658
+ margin-bottom: var(--space-8);
659
+ }
660
+
661
+ .feature-guide-title { /* L2 */
662
+ font-size: var(--text-5);
663
+ font-weight: var(--weight-7);
664
+ margin-bottom: var(--space-3);
665
+ }
666
+
667
+ .feature-guide-tagline { /* L4 */
668
+ font-size: var(--text-3);
669
+ color: var(--text-muted);
670
+ line-height: var(--leading-2);
671
+ }
672
+
673
+ .feature-guide-section {
674
+ margin-bottom: var(--space-7);
675
+ }
676
+
677
+ .feature-guide-heading { /* L3 */
678
+ font-size: var(--text-3);
679
+ font-weight: var(--weight-6);
680
+ margin-bottom: var(--space-3);
681
+ }
682
+
683
+ .feature-guide-body { /* L4 */
684
+ font-size: var(--text-2);
685
+ color: var(--text-muted);
686
+ line-height: var(--leading-3);
687
+ }
688
+
689
+ .feature-guide-tips {
690
+ padding-left: var(--space-5);
691
+ margin: 0;
692
+ }
693
+
694
+ .feature-guide-tips li { /* L4 */
695
+ font-size: var(--text-2);
696
+ color: var(--text-muted);
697
+ line-height: var(--leading-3);
698
+ margin-bottom: var(--space-2);
699
+ }
700
+
701
+ .feature-guide-back {
702
+ text-align: center;
703
+ margin-top: var(--space-8);
704
+ }
@@ -34,6 +34,38 @@
34
34
  cursor: pointer;
35
35
  }
36
36
 
37
+ .llm-model-item {
38
+ display: flex;
39
+ gap: 0.75em;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ }
43
+
44
+ .llm-model-name {
45
+ min-width: 0;
46
+ overflow-wrap: anywhere;
47
+ }
48
+
49
+ .llm-model-delete {
50
+ flex: 0 0 auto;
51
+ width: 1.75em;
52
+ height: 1.75em;
53
+ padding: 0;
54
+ color: var(--text-muted);
55
+ font: inherit;
56
+ line-height: 1;
57
+ cursor: pointer;
58
+ background: transparent;
59
+ border: 0;
60
+ border-radius: var(--radius-round);
61
+ }
62
+
63
+ .llm-model-delete:hover,
64
+ .llm-model-delete:focus-visible {
65
+ color: var(--text-primary);
66
+ background: var(--surface-hover);
67
+ }
68
+
37
69
  .mention-item:hover,
38
70
  .mention-item.active,
39
71
  .common-popup-item:hover,
@@ -105,6 +105,20 @@ body.chat-fullscreen #topic-list-modal {
105
105
  display: flex;
106
106
  flex-direction: column;
107
107
  max-width: 90vw;
108
+ /* CommonPopup sets max-height inline from the space available beside the
109
+ anchor. Border-box keeps that number the rendered height (it is measured
110
+ back via offsetHeight), and hiding overflow stops content from spilling
111
+ past the cap instead of scrolling inside it. */
112
+ box-sizing: border-box;
113
+ overflow: hidden;
114
+ }
115
+
116
+ /* The scrollable list is what gives when the popup is capped: flex items do not
117
+ shrink past their content by default, so without this the list keeps its
118
+ max-height and the bottom of it is simply clipped away. */
119
+ .common-popup-list {
120
+ flex: 1 1 auto;
121
+ min-height: 0;
108
122
  }
109
123
 
110
124
  .common-popup input[type="text"] {
@@ -10,7 +10,6 @@
10
10
  .creative-row-actions,
11
11
  .creative-toggle-btn,
12
12
  .select-creative-checkbox,
13
- #creative-guide-popover,
14
13
  #creative-guide-link,
15
14
  #inbox-menu-btn,
16
15
  #plans-menu-btn,
@@ -41,8 +41,8 @@
41
41
  text-overflow: ellipsis;
42
42
  }
43
43
 
44
+ /* The selected surface is drawn by the overlay pseudo-element in gnb.css. */
44
45
  .search-popup-trigger.active {
45
- background: var(--surface-hover);
46
46
  color: var(--text-primary);
47
47
  font-weight: var(--weight-5);
48
48
  }
@@ -0,0 +1,3 @@
1
+ .masked-secret-field {
2
+ -webkit-text-security: disc;
3
+ }
@@ -0,0 +1,307 @@
1
+ body.creative-workspace {
2
+ height: 100dvh;
3
+ overflow: hidden;
4
+ }
5
+
6
+ .creative-workspace-shell {
7
+ display: grid;
8
+ grid-template-columns: minmax(240px, 300px) minmax(0, 1fr) minmax(0, 1fr);
9
+ flex: 1;
10
+ width: 100%;
11
+ min-height: 0;
12
+ }
13
+
14
+ body.creative-workspace > nav {
15
+ max-width: none;
16
+ }
17
+
18
+ .creative-workspace-shell > main {
19
+ grid-column: 2;
20
+ width: 100%;
21
+ max-width: none;
22
+ min-width: 0;
23
+ overflow: auto;
24
+ }
25
+
26
+ #creative-workspace-content {
27
+ display: block;
28
+ min-width: 0;
29
+ }
30
+
31
+ body.creative-workspace .creative-content,
32
+ body.creative-workspace .creative-title-content {
33
+ min-width: 0;
34
+ max-width: none;
35
+ }
36
+
37
+ /* The rule above drops the viewport-based max-width that used to cap row width.
38
+ Without it, the flex wrappers between the grid column and .creative-content
39
+ keep their default min-width: auto, so they refuse to shrink below the
40
+ min-content width of their contents. Text still wraps (min-content is one
41
+ word), but unbreakable blocks -- code blocks and tables -- overflow the
42
+ column. Zeroing min-width lets the flex chain shrink to the column.
43
+
44
+ The title row (the root creative) has its own chain: .creative-row-start >
45
+ h1.page-title > .creative-title-content. Shrinking only .creative-row-start
46
+ would let the title's min-content width paint through the shrunken parent,
47
+ so .page-title belongs in the same list. */
48
+ body.creative-workspace .creative-row-start,
49
+ body.creative-workspace .creative-tree-li,
50
+ body.creative-workspace .page-title,
51
+ body.creative-workspace .indent1,
52
+ body.creative-workspace .indent2,
53
+ body.creative-workspace .indent3 {
54
+ min-width: 0;
55
+ }
56
+
57
+ .creative-workspace-tree-region {
58
+ grid-column: 1;
59
+ min-width: 0;
60
+ overflow: hidden;
61
+ background: var(--surface-section);
62
+ border-right: var(--border-1) solid var(--border-color);
63
+ }
64
+
65
+ #creative-workspace-tree {
66
+ display: flex;
67
+ flex-direction: column;
68
+ height: 100%;
69
+ min-height: 0;
70
+ }
71
+
72
+ .creative-workspace-tree-header {
73
+ padding: var(--space-3);
74
+ border-bottom: var(--border-1) solid var(--border-color);
75
+ }
76
+
77
+ .creative-workspace-tree-header h2 {
78
+ margin: 0;
79
+ font-size: var(--text-2);
80
+ }
81
+
82
+ .creative-workspace-tree-nav {
83
+ display: block;
84
+ flex: 1;
85
+ max-width: none;
86
+ min-height: 0;
87
+ padding: var(--space-2);
88
+ overflow: auto;
89
+ overscroll-behavior-y: contain;
90
+ background: transparent;
91
+ border: 0;
92
+ }
93
+
94
+ .creative-workspace-tree-list {
95
+ margin: 0;
96
+ padding: 0 0 0 var(--space-3);
97
+ list-style: none;
98
+ }
99
+
100
+ .creative-workspace-tree-nav > .creative-workspace-tree-list {
101
+ padding-left: 0;
102
+ }
103
+
104
+ .creative-workspace-tree-row {
105
+ display: flex;
106
+ align-items: center;
107
+ min-width: 0;
108
+ }
109
+
110
+ .creative-workspace-tree-branch-toggle,
111
+ .creative-workspace-tree-branch-spacer {
112
+ width: 1.75rem;
113
+ min-width: 1.75rem;
114
+ }
115
+
116
+ .creative-workspace-tree-branch-toggle {
117
+ padding: var(--space-1);
118
+ color: var(--text-muted);
119
+ background: transparent;
120
+ border: 0;
121
+ cursor: pointer;
122
+ }
123
+
124
+ .creative-workspace-tree-link {
125
+ display: block;
126
+ flex: 1;
127
+ min-width: 0;
128
+ padding: var(--space-2);
129
+ overflow: hidden;
130
+ color: var(--text-primary);
131
+ text-overflow: ellipsis;
132
+ white-space: nowrap;
133
+ border-radius: var(--radius-2);
134
+ }
135
+
136
+ .creative-workspace-tree-link:hover,
137
+ .creative-workspace-tree-link.is-current {
138
+ background: var(--surface-hover);
139
+ }
140
+
141
+ .creative-workspace-tree-link.is-current {
142
+ color: var(--color-active);
143
+ font-weight: var(--weight-6);
144
+ }
145
+
146
+ .creative-workspace-tree-status {
147
+ padding: var(--space-3);
148
+ color: var(--text-muted);
149
+ text-align: center;
150
+ }
151
+
152
+ .creative-workspace-tree-toggle {
153
+ display: none;
154
+ }
155
+
156
+ #comments-list.docked-empty {
157
+ align-items: center;
158
+ justify-content: center;
159
+ color: var(--text-muted);
160
+ text-align: center;
161
+ background: transparent;
162
+ }
163
+
164
+ #comments-list.docked-empty::before {
165
+ display: none;
166
+ }
167
+
168
+ @media (min-width: 768px) {
169
+ .creative-workspace-shell > #comments-popup[data-docked="true"]:not([data-fullscreen="true"]) {
170
+ display: flex;
171
+ position: static;
172
+ grid-column: 3;
173
+ width: auto;
174
+ height: auto;
175
+ max-width: none !important;
176
+ max-height: none;
177
+ min-width: 0;
178
+ min-height: 0;
179
+ z-index: auto;
180
+ border-width: 0 0 0 var(--border-1);
181
+ border-color: var(--border-color);
182
+ border-radius: 0;
183
+ box-shadow: none;
184
+ transition: none;
185
+ }
186
+
187
+ .creative-workspace-shell > #comments-popup[data-docked="true"]:not([data-fullscreen="true"]) .resize-handle {
188
+ display: none;
189
+ }
190
+
191
+ .creative-workspace-shell:has(#comments-popup.docked-collapsed) {
192
+ grid-template-columns: minmax(240px, 300px) minmax(0, 1fr) 3rem;
193
+ }
194
+
195
+ #comments-popup.docked-collapsed:not([data-fullscreen="true"]) {
196
+ padding: var(--space-2);
197
+ }
198
+
199
+ #comments-popup.docked-collapsed:not([data-fullscreen="true"]) > :not(.comments-popup-header),
200
+ #comments-popup.docked-collapsed:not([data-fullscreen="true"]) .comments-popup-header > :not(.comments-popup-actions),
201
+ #comments-popup.docked-collapsed:not([data-fullscreen="true"]) .comments-popup-actions > :not(#close-comments-btn) {
202
+ display: none !important;
203
+ }
204
+ }
205
+
206
+ @media (min-width: 768px) and (max-width: 1279px) {
207
+ .creative-workspace-shell {
208
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
209
+ }
210
+
211
+ .creative-workspace-shell > main {
212
+ grid-column: 1;
213
+ }
214
+
215
+ .creative-workspace-shell > #comments-popup[data-docked="true"]:not([data-fullscreen="true"]) {
216
+ grid-column: 2;
217
+ }
218
+
219
+ .creative-workspace-shell:has(#comments-popup.docked-collapsed) {
220
+ grid-template-columns: minmax(0, 1fr) 3rem;
221
+ }
222
+ }
223
+
224
+ /* Below the three-column breakpoint the tree gives up its own column and
225
+ becomes an off-canvas drawer with a toggle handle. Shared by the two-panel
226
+ band and the one-panel (mobile) band so both collapse the same way — and so
227
+ the tree stays mounted in both, preserving its expansion state. */
228
+ @media (max-width: 1279px) {
229
+ .creative-workspace-tree-region {
230
+ position: fixed;
231
+ top: 0;
232
+ bottom: 0;
233
+ left: 0;
234
+ z-index: var(--layer-modal);
235
+ width: min(300px, 80vw);
236
+ overflow: visible;
237
+ box-shadow: var(--shadow-3);
238
+ transform: translateX(-100%);
239
+ transition: transform 0.2s var(--ease-out-2);
240
+ }
241
+
242
+ .creative-workspace-tree-region > aside {
243
+ visibility: hidden;
244
+ }
245
+
246
+ .creative-workspace-tree-region.is-open {
247
+ transform: translateX(0);
248
+ }
249
+
250
+ .creative-workspace-tree-region.is-open > aside {
251
+ visibility: visible;
252
+ }
253
+
254
+ .creative-workspace-tree-toggle {
255
+ position: absolute;
256
+ top: 5rem;
257
+ right: -2.75rem;
258
+ display: inline-flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ width: 2.75rem;
262
+ height: 2.75rem;
263
+ padding: 0;
264
+ color: var(--text-primary);
265
+ background: var(--surface-section);
266
+ border: var(--border-1) solid var(--border-color);
267
+ border-left: 0;
268
+ border-radius: 0 var(--radius-2) var(--radius-2) 0;
269
+ cursor: pointer;
270
+ }
271
+
272
+ .creative-workspace-tree-region.is-open .creative-workspace-tree-toggle-icon {
273
+ transform: rotate(180deg);
274
+ }
275
+ }
276
+
277
+ @media (max-width: 767px) {
278
+ body.creative-workspace {
279
+ height: auto;
280
+ overflow: visible;
281
+ }
282
+
283
+ /* The floating chat is rendered after the tree in the workspace shell and
284
+ uses the modal layer. Keep the opened tree above it so its rows remain
285
+ visible and tappable on one-panel screens. */
286
+ .creative-workspace-tree-region {
287
+ z-index: calc(var(--layer-modal) + 1);
288
+ }
289
+
290
+ .creative-workspace-shell {
291
+ display: block;
292
+ }
293
+
294
+ body.creative-workspace > nav,
295
+ .creative-workspace-shell > main {
296
+ max-width: var(--max-width);
297
+ }
298
+
299
+ /* In the two-panel band the handle's default offset lands in the left
300
+ gutter main leaves itself. One-panel main runs flush to the edge, so the
301
+ same offset would sit on top of the breadcrumb row — park the handle in
302
+ the nav's empty left corner instead, where it reads as the menu button
303
+ it effectively is. */
304
+ .creative-workspace-tree-toggle {
305
+ top: var(--space-3);
306
+ }
307
+ }
@@ -28,14 +28,14 @@ module Collavre
28
28
 
29
29
  # When the MCP process crashes or the WebSocket drops before the plugin
30
30
  # can call DELETE /api/v1/agent/:id, a Claude Channel session would
31
- # otherwise stay matchable (routing_expression: "true") and any future
31
+ # otherwise stay matchable and any future
32
32
  # comment on a creative still shared with the agent would dispatch into an
33
33
  # empty stream — delegated work that only clears via stuck recovery.
34
34
  #
35
35
  # One shared agent can have MANY concurrent sessions, so routing is gated
36
36
  # on PRESENCE: this session drops its own AgentSubscription row, and
37
- # routing is cleared only when NO rows remain. A still-live sibling session
38
- # keeps its row, so routing stays active and its in-flight work is never
37
+ # routing is enabled only while a row is live. A still-live sibling session
38
+ # keeps its row, so dispatch remains active and its in-flight work is never
39
39
  # cancelled. Done under the agent's row lock so a concurrent subscribe on
40
40
  # the same agent serializes (no clear-vs-activate race).
41
41
  #
@@ -47,7 +47,7 @@ module Collavre
47
47
  def unsubscribed
48
48
  return unless @session_agent && @subscription_token
49
49
 
50
- cleared = false
50
+ last_session_disconnected = false
51
51
  dropped_session_id = nil
52
52
  @session_agent.with_lock do
53
53
  scope = AgentSubscription.where(agent_id: @session_agent.id, token: @subscription_token)
@@ -67,25 +67,21 @@ module Collavre
67
67
  # row cannot masquerade as a live sibling and pin routing on forever.
68
68
  AgentSubscription.reap_stale!(@session_agent.id)
69
69
 
70
- # Another session is still LIVE on this shared agent. Keep routing
71
- # active; whichever session unsubscribes last clears it.
70
+ # Another session is still LIVE on this shared agent. Its presence keeps
71
+ # dispatch active, so only the final disconnect owns agent-wide cleanup.
72
72
  next if AgentSubscription.live.where(agent_id: @session_agent.id).exists?
73
73
 
74
- @session_agent.update_columns(
75
- routing_expression: nil,
76
- routing_subscription_token: nil
77
- )
78
- cleared = true
74
+ @session_agent.update_columns(routing_subscription_token: nil)
75
+ last_session_disconnected = true
79
76
  end
80
77
 
81
- if cleared
78
+ if last_session_disconnected
82
79
  # Reconnect-grace cancellation (last session): clearing routing only
83
80
  # makes the agent unroutable. Any task already "delegated" still holds
84
81
  # its ResourceTracker slot — the dispatch was broadcast to a now-dead
85
82
  # stream so no client remains to call /reply, and the slot would stay
86
83
  # held until StuckDetectorJob times out. The job cancels those tasks
87
- # after a grace window, but only if the agent is still offline (it
88
- # rechecks routing_expression AND that no session has resubscribed).
84
+ # after a grace window, but only if the agent is still offline.
89
85
  CancelOfflineDelegatedTasksJob
90
86
  .set(wait: CancelOfflineDelegatedTasksJob::GRACE_SECONDS.seconds)
91
87
  .perform_later(@session_agent.id, @subscription_token)
@@ -153,8 +149,8 @@ module Collavre
153
149
  return reject unless agent&.ai_user?
154
150
  return reject unless agent.created_by_id == current_user.id
155
151
 
156
- # Attach the stream BEFORE activating routing. Orchestration::Matcher
157
- # can pick this agent as soon as routing_expression becomes "true";
152
+ # Attach the stream BEFORE recording presence. Orchestration::Matcher
153
+ # can pick this agent as soon as its presence row is committed;
158
154
  # broadcasts to agent:user:<id> in the window between the UPDATE
159
155
  # committing and stream_from registering the subscription would land
160
156
  # in a stream with no subscriber, leaving the new delegated task
@@ -165,7 +161,7 @@ module Collavre
165
161
  @subscription_token = SecureRandom.hex(8) if agent.claude_channel_agent?
166
162
  stream_from "agent:user:#{agent.id}"
167
163
 
168
- # Record this session's presence row and (re)activate routing under the
164
+ # Record this session's presence row under the
169
165
  # agent's row lock, so a concurrent unsubscribe on the same shared agent
170
166
  # serializes against this and cannot clear routing out from under a live
171
167
  # session. routing_subscription_token is kept as the most-recent-session
@@ -185,10 +181,7 @@ module Collavre
185
181
  token: @subscription_token,
186
182
  session_id: params[:session_id].presence
187
183
  )
188
- agent.update_columns(
189
- routing_subscription_token: @subscription_token,
190
- routing_expression: "true"
191
- )
184
+ agent.update_columns(routing_subscription_token: @subscription_token)
192
185
  end
193
186
  end
194
187
  end