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.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
- data/app/assets/stylesheets/collavre/creatives.css +346 -37
- data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
- data/app/assets/stylesheets/collavre/gnb.css +64 -3
- data/app/assets/stylesheets/collavre/landing.css +108 -3
- data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +14 -0
- data/app/assets/stylesheets/collavre/print.css +0 -1
- data/app/assets/stylesheets/collavre/search_popup.css +1 -1
- data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
- data/app/assets/stylesheets/collavre/workspace.css +307 -0
- data/app/channels/collavre/agent_channel.rb +14 -21
- data/app/channels/collavre/comments_presence_channel.rb +181 -21
- data/app/channels/collavre/inbox_badge_channel.rb +7 -1
- data/app/components/collavre/inbox/badge_component.rb +6 -13
- data/app/components/collavre/progress_filter_component.html.erb +2 -2
- data/app/components/collavre/progress_filter_component.rb +20 -0
- data/app/controllers/collavre/admin/settings_controller.rb +4 -1
- data/app/controllers/collavre/agent_connections_controller.rb +165 -0
- data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
- data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
- data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
- data/app/controllers/collavre/application_controller.rb +13 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
- data/app/controllers/collavre/comments_controller.rb +45 -131
- data/app/controllers/collavre/concerns/shareable.rb +9 -3
- data/app/controllers/collavre/creatives_controller.rb +144 -11
- data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
- data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
- data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
- data/app/controllers/collavre/features_controller.rb +57 -0
- data/app/controllers/collavre/invites_controller.rb +12 -1
- data/app/controllers/collavre/llm_models_controller.rb +8 -0
- data/app/controllers/collavre/notices_controller.rb +20 -0
- data/app/controllers/collavre/tasks_controller.rb +40 -6
- data/app/controllers/collavre/topics_controller.rb +130 -53
- data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
- data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
- data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
- data/app/errors/collavre/turn_deadline_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +38 -0
- data/app/helpers/collavre/creatives_helper.rb +56 -22
- data/app/helpers/collavre/navigation_helper.rb +12 -0
- data/app/javascript/collavre.js +1 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
- data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
- data/app/javascript/components/creative_tree_row.js +62 -22
- data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
- data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
- data/app/javascript/controllers/__tests__/index.test.js +42 -0
- data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
- data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
- data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
- data/app/javascript/controllers/agent_connection_controller.js +357 -0
- data/app/javascript/controllers/agent_vendor_controller.js +23 -0
- data/app/javascript/controllers/comment_controller.js +3 -1
- data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
- data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
- data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
- data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
- data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
- data/app/javascript/controllers/comments/form_controller.js +795 -14
- data/app/javascript/controllers/comments/list_controller.js +338 -52
- data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
- data/app/javascript/controllers/comments/popup_controller.js +425 -83
- data/app/javascript/controllers/comments/presence_controller.js +317 -55
- data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
- data/app/javascript/controllers/comments/topics_controller.js +1772 -100
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
- data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
- data/app/javascript/controllers/creatives/tree_controller.js +140 -31
- data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
- data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
- data/app/javascript/controllers/gateway_check_controller.js +36 -0
- data/app/javascript/controllers/index.js +25 -1
- data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
- data/app/javascript/controllers/link_creative_controller.js +89 -18
- data/app/javascript/controllers/progress_filter_controller.js +12 -22
- data/app/javascript/controllers/search_popup_controller.js +64 -46
- data/app/javascript/controllers/share_user_search_controller.js +2 -2
- data/app/javascript/controllers/topic_list_controller.js +44 -7
- data/app/javascript/controllers/workspace_tree_controller.js +662 -0
- data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
- data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
- data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
- data/app/javascript/creatives/tree_renderer.js +65 -22
- data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
- data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
- data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
- data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
- data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
- data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
- data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
- data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
- data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
- data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
- data/app/javascript/lib/api/creatives.js +32 -13
- data/app/javascript/lib/api/csrf_fetch.js +6 -4
- data/app/javascript/lib/api/topics.js +63 -5
- data/app/javascript/lib/chat_drafts.js +786 -0
- data/app/javascript/lib/common_popup.js +164 -25
- data/app/javascript/lib/creative_link_prefetch.js +27 -0
- data/app/javascript/lib/last_visited_creative.js +97 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
- data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
- data/app/javascript/lib/lexical/creative_link_node.js +101 -0
- data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
- data/app/javascript/lib/lexical/link_toolbar.js +23 -0
- data/app/javascript/lib/turbo_stream_actions.js +23 -16
- data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
- data/app/javascript/lib/utils/filter_navigation.js +100 -0
- data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
- data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
- data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
- data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
- data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
- data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
- data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
- data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
- data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
- data/app/javascript/modules/command_args_form.js +0 -12
- data/app/javascript/modules/command_menu.js +80 -38
- data/app/javascript/modules/creative_link_picker.js +56 -0
- data/app/javascript/modules/creative_row_editor.js +446 -243
- data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
- data/app/javascript/modules/creative_tree_dom.js +4 -0
- data/app/javascript/modules/creative_tree_empty_state.js +80 -0
- data/app/javascript/modules/dom_listener_registry.js +23 -0
- data/app/javascript/modules/mention_menu.js +2 -3
- data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
- data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
- data/app/javascript/utils/caret_position.js +10 -0
- data/app/javascript/utils/chevron_icons.js +7 -0
- data/app/jobs/collavre/ai_agent_job.rb +262 -72
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
- data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
- data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
- data/app/jobs/collavre/comment_notification_job.rb +14 -0
- data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
- data/app/jobs/collavre/compress_job.rb +33 -22
- data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
- data/app/jobs/collavre/cron_action_job.rb +36 -18
- data/app/jobs/collavre/drop_trigger_job.rb +23 -9
- data/app/jobs/collavre/merge_comments_job.rb +33 -16
- data/app/jobs/collavre/push_notification_job.rb +62 -18
- data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
- data/app/jobs/collavre/stuck_detector_job.rb +1 -1
- data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
- data/app/models/collavre/agent_gateway.rb +200 -0
- data/app/models/collavre/agent_subscription.rb +4 -4
- data/app/models/collavre/agent_workspace.rb +210 -0
- data/app/models/collavre/comment/broadcastable.rb +22 -52
- data/app/models/collavre/comment/notifiable.rb +154 -77
- data/app/models/collavre/comment/topic_membership.rb +41 -0
- data/app/models/collavre/comment.rb +336 -31
- data/app/models/collavre/comment_notification_delivery.rb +61 -0
- data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
- data/app/models/collavre/comment_presence_store.rb +272 -12
- data/app/models/collavre/comment_read_pointer.rb +9 -15
- data/app/models/collavre/creative/describable.rb +8 -2
- data/app/models/collavre/creative/permissible.rb +11 -0
- data/app/models/collavre/creative.rb +29 -4
- data/app/models/collavre/creative_share.rb +109 -4
- data/app/models/collavre/current.rb +1 -0
- data/app/models/collavre/llm_model.rb +62 -0
- data/app/models/collavre/system_setting.rb +12 -1
- data/app/models/collavre/task.rb +121 -2
- data/app/models/collavre/topic.rb +51 -0
- data/app/models/collavre/user.rb +101 -41
- data/app/models/collavre/user_creative_preference.rb +3 -1
- data/app/services/collavre/agent_provisioning/archive.rb +108 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
- data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
- data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
- data/app/services/collavre/ai_agent/message_builder.rb +103 -16
- data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
- data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
- data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
- data/app/services/collavre/ai_agent_service.rb +108 -20
- data/app/services/collavre/ai_client.rb +211 -21
- data/app/services/collavre/cli_proxy/client.rb +147 -0
- data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
- data/app/services/collavre/cli_proxy/identity.rb +47 -0
- data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
- data/app/services/collavre/command_menu_service.rb +0 -10
- data/app/services/collavre/comment_move_service.rb +96 -7
- data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
- data/app/services/collavre/comments/action_executor.rb +48 -20
- data/app/services/collavre/comments/command_processor.rb +0 -1
- data/app/services/collavre/comments/list_response.rb +133 -0
- data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
- data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
- data/app/services/collavre/comments/read_receipt_index.rb +152 -0
- data/app/services/collavre/comments/topic_command.rb +72 -1
- data/app/services/collavre/comments/topic_mutation.rb +20 -0
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
- data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
- data/app/services/collavre/creatives/reorderer.rb +15 -3
- data/app/services/collavre/creatives/tree_builder.rb +1 -1
- data/app/services/collavre/creatives/tree_formatter.rb +5 -6
- data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
- data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
- data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
- data/app/services/collavre/home_page_path.rb +55 -0
- data/app/services/collavre/html_text.rb +64 -0
- data/app/services/collavre/http_client.rb +11 -2
- data/app/services/collavre/inbox_reply_service.rb +1 -1
- data/app/services/collavre/markdown_converter.rb +13 -1
- data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
- data/app/services/collavre/orchestration/arbiter.rb +48 -5
- data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
- data/app/services/collavre/orchestration/delivery_record.rb +792 -0
- data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
- data/app/services/collavre/orchestration/matcher.rb +132 -17
- data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
- data/app/services/collavre/orchestration/scheduler.rb +10 -4
- data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
- data/app/services/collavre/orchestration/selection.rb +43 -0
- data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
- data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
- data/app/services/collavre/orchestration/topic_slot.rb +73 -0
- data/app/services/collavre/system_events/context_builder.rb +60 -11
- data/app/services/collavre/system_events/dispatcher.rb +58 -12
- data/app/services/collavre/system_events/envelope.rb +73 -0
- data/app/services/collavre/system_events/vocabulary.rb +45 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
- data/app/services/collavre/tools/cron_create_service.rb +32 -18
- data/app/services/collavre/tools/id_list.rb +38 -0
- data/app/services/collavre/tools/topic_authorizer.rb +61 -7
- data/app/services/collavre/tools/topic_branch_service.rb +85 -0
- data/app/services/collavre/tools/topic_create_service.rb +117 -0
- data/app/services/collavre/tools/topic_list_service.rb +101 -0
- data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
- data/app/services/collavre/tools/topic_messages_service.rb +346 -0
- data/app/services/collavre/tools/topic_move_service.rb +108 -0
- data/app/services/collavre/tools/topic_selection.rb +59 -0
- data/app/services/collavre/tools/topic_update_service.rb +165 -0
- data/app/services/collavre/topic_branch_service.rb +39 -10
- data/app/services/collavre/topics/agent_resolver.rb +87 -0
- data/app/services/collavre/topics/char_budget.rb +70 -0
- data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
- data/app/services/collavre/topics/message_page.rb +380 -0
- data/app/services/collavre/topics/message_page_markdown.rb +102 -0
- data/app/services/collavre/topics/message_scope.rb +61 -0
- data/app/services/collavre/topics/message_stats.rb +92 -0
- data/app/services/collavre/topics/move_blocker.rb +47 -0
- data/app/services/collavre/topics/next_name.rb +35 -0
- data/app/services/collavre/topics/pin_rejection.rb +25 -0
- data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
- data/app/services/collavre/topics/reserved_name.rb +18 -0
- data/app/services/collavre/topics/serializer.rb +92 -0
- data/app/services/collavre/topics/topic_destroy.rb +30 -0
- data/app/services/collavre/topics/topic_move.rb +80 -0
- data/app/services/collavre/topics/topic_move_effects.rb +35 -0
- data/app/services/collavre/topics/topic_mutation.rb +27 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
- data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
- data/app/views/collavre/agent_connections/show.html.erb +77 -0
- data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
- data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
- data/app/views/collavre/agent_gateways/index.html.erb +54 -0
- data/app/views/collavre/agent_gateways/new.html.erb +2 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
- data/app/views/collavre/comments/_empty_state.html.erb +33 -0
- data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
- data/app/views/collavre/comments/_feature_card.html.erb +35 -0
- data/app/views/collavre/comments/_list.html.erb +3 -2
- data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
- data/app/views/collavre/creatives/_add_button.html.erb +17 -19
- data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
- data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
- data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
- data/app/views/collavre/creatives/index.html.erb +79 -18
- data/app/views/collavre/desktop_setup/show.html.erb +114 -0
- data/app/views/collavre/features/index.html.erb +42 -0
- data/app/views/collavre/features/show.html.erb +52 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
- data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
- data/app/views/collavre/invites/show.html.erb +14 -1
- data/app/views/collavre/landing/show.html.erb +5 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
- data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
- data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
- data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
- data/app/views/collavre/user_themes/index.html.erb +1 -1
- data/app/views/collavre/users/_llm_fields.html.erb +65 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
- data/app/views/collavre/users/edit_ai.html.erb +33 -31
- data/app/views/collavre/users/new.html.erb +4 -2
- data/app/views/collavre/users/new_ai.html.erb +14 -32
- data/app/views/collavre/users/show.html.erb +13 -1
- data/app/views/layouts/collavre/landing.html.erb +9 -4
- data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
- data/config/locales/admin.en.yml +3 -1
- data/config/locales/admin.ko.yml +3 -1
- data/config/locales/agent_gateway.en.yml +217 -0
- data/config/locales/agent_gateway.ko.yml +217 -0
- data/config/locales/ai_agent.en.yml +0 -1
- data/config/locales/ai_agent.ko.yml +0 -1
- data/config/locales/comments.en.yml +72 -30
- data/config/locales/comments.ko.yml +72 -30
- data/config/locales/creatives.en.yml +32 -1
- data/config/locales/creatives.ko.yml +32 -1
- data/config/locales/features.en.yml +140 -0
- data/config/locales/features.ko.yml +140 -0
- data/config/locales/invites.en.yml +3 -0
- data/config/locales/invites.ko.yml +3 -0
- data/config/locales/landing.en.yml +2 -1
- data/config/locales/landing.ko.yml +2 -1
- data/config/locales/tools.en.yml +20 -0
- data/config/locales/tools.ko.yml +20 -0
- data/config/locales/users.en.yml +15 -14
- data/config/locales/users.ko.yml +14 -14
- data/config/routes.rb +45 -19
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
- data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
- data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
- data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
- data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
- data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
- data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
- data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
- data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
- data/db/migrate/20260809000001_create_llm_models.rb +64 -0
- data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
- data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
- data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
- data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
- data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
- data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
- data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
- data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
- data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
- data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
- data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
- data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
- data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
- data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
- data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
- data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
- data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
- data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
- data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
- data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
- data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
- data/db/seeds.rb +1 -51
- data/lib/collavre/engine.rb +108 -1
- data/lib/collavre/feature_card_registry.rb +168 -0
- data/lib/collavre/hashed_access_token_lookup.rb +22 -0
- data/lib/collavre/sensitive_request_silencer.rb +23 -0
- data/lib/collavre/user_extensions.rb +0 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +1 -0
- data/skills/collavre/SKILL.md +165 -0
- data/skills/collavre/package.json +8 -0
- data/skills/collavre/references/tool-reference.md +347 -0
- data/skills/collavre/scripts/collavre +634 -0
- data/skills/collavre/scripts/html_entities.js +24 -0
- metadata +228 -30
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
- data/app/controllers/collavre/inbox_items_controller.rb +0 -70
- data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
- data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
- data/app/javascript/lib/typo_correction.js +0 -146
- data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
- data/app/javascript/modules/creative_guide.js +0 -67
- data/app/javascript/modules/typo_corrector.js +0 -534
- data/app/jobs/collavre/inbox_summary_job.rb +0 -24
- data/app/mailers/collavre/inbox_mailer.rb +0 -19
- data/app/models/collavre/inbox_item.rb +0 -89
- data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
- data/app/services/collavre/comments/work_command.rb +0 -161
- data/app/services/collavre/comments/workflow_executor.rb +0 -276
- data/app/services/collavre/gemini_parent_recommender.rb +0 -77
- data/app/services/collavre/mobile/event_summarizer.rb +0 -40
- data/app/services/collavre/typo_corrector.rb +0 -188
- data/app/views/collavre/inbox_items/_item.html.erb +0 -18
- data/app/views/collavre/inbox_items/_items.html.erb +0 -3
- data/app/views/collavre/inbox_items/_list.html.erb +0 -7
- data/app/views/collavre/inbox_items/index.html.erb +0 -1
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
- data/app/views/collavre/users/typo_correction.html.erb +0 -50
- data/config/locales/mobile.en.yml +0 -16
- data/config/locales/mobile.ko.yml +0 -16
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class AgentProvisioningController < ActionController::Base
|
|
5
|
+
SKILL_NAME = "collavre"
|
|
6
|
+
CONFIG_NAME = "collavre"
|
|
7
|
+
CACHE_CONTROL = "public, max-age=31536000, immutable"
|
|
8
|
+
|
|
9
|
+
rate_limit to: 60, within: 1.minute, only: :manifest
|
|
10
|
+
|
|
11
|
+
def manifest
|
|
12
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
13
|
+
workspace = find_workspace!
|
|
14
|
+
skill = Collavre::AgentProvisioning::Archive.collavre_skill
|
|
15
|
+
config = Collavre::AgentProvisioning::Archive.workspace_config(workspace, base_url: request.base_url)
|
|
16
|
+
|
|
17
|
+
render json: {
|
|
18
|
+
schema: "agent-provisioning/v1",
|
|
19
|
+
items: [
|
|
20
|
+
{
|
|
21
|
+
type: "skill",
|
|
22
|
+
name: SKILL_NAME,
|
|
23
|
+
url: absolute_url(agent_provision_skill_path(sha256: digest(skill))),
|
|
24
|
+
sha256: digest(skill)
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "config",
|
|
28
|
+
name: CONFIG_NAME,
|
|
29
|
+
url: absolute_url(agent_provision_config_path(
|
|
30
|
+
agent_id: workspace.agent_id,
|
|
31
|
+
token: workspace.manifest_token,
|
|
32
|
+
sha256: digest(config)
|
|
33
|
+
)),
|
|
34
|
+
sha256: digest(config)
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def skill
|
|
41
|
+
bytes = Collavre::AgentProvisioning::Archive.collavre_skill
|
|
42
|
+
return head :not_found unless digest_matches?(bytes)
|
|
43
|
+
|
|
44
|
+
send_archive(bytes, "collavre.tar.gz")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def config_archive
|
|
48
|
+
workspace = find_workspace!
|
|
49
|
+
bytes = Collavre::AgentProvisioning::Archive.workspace_config(workspace, base_url: request.base_url)
|
|
50
|
+
return head :not_found unless digest_matches?(bytes)
|
|
51
|
+
|
|
52
|
+
send_archive(bytes, "collavre-config.tar.gz", cache_control: "private, no-store")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def find_workspace!
|
|
58
|
+
Collavre::AgentWorkspace.find_by_manifest_token!(agent_id: params[:agent_id], token: params[:token])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def digest(bytes)
|
|
62
|
+
Collavre::AgentProvisioning::Archive.sha256(bytes)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def absolute_url(path)
|
|
66
|
+
"#{request.base_url}#{path}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def digest_matches?(bytes)
|
|
70
|
+
supplied = params[:sha256].to_s
|
|
71
|
+
supplied.bytesize == 64 && ActiveSupport::SecurityUtils.secure_compare(supplied, digest(bytes))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def send_archive(bytes, filename, cache_control: CACHE_CONTROL)
|
|
75
|
+
response.headers["Cache-Control"] = cache_control
|
|
76
|
+
send_data bytes, type: "application/gzip", disposition: "inline", filename: filename
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -95,7 +95,7 @@ module Collavre
|
|
|
95
95
|
# row yet. Drop it up front — keyed by the ending session's id (sent by
|
|
96
96
|
# the plugin, or derived from the required topic) — so this session's
|
|
97
97
|
# own still-live row cannot masquerade as a live sibling and skip the
|
|
98
|
-
# last-session teardown (which would
|
|
98
|
+
# last-session teardown (which would leave the agent apparently live and, if
|
|
99
99
|
# the WS close is never processed, leave nothing to clear it). Done
|
|
100
100
|
# under the agent row lock to serialize against a concurrent subscribe/
|
|
101
101
|
# unsubscribe on the shared agent — the same lock AgentChannel uses.
|
|
@@ -110,14 +110,8 @@ module Collavre
|
|
|
110
110
|
|
|
111
111
|
unless sibling_sessions_live?(ai_user)
|
|
112
112
|
last_session = true
|
|
113
|
-
# Last (or only) session for this agent.
|
|
114
|
-
#
|
|
115
|
-
# dispatching new comments to this now-clientless agent while we
|
|
116
|
-
# drain its task graph below; otherwise a comment arriving mid-drain
|
|
117
|
-
# could enqueue a new task right after we cancelled the last one.
|
|
118
|
-
# (Routing is reactivated on the next AgentChannel subscribe, not on
|
|
119
|
-
# re-register.)
|
|
120
|
-
ai_user.update_column(:routing_expression, nil) if ai_user.routing_expression.present?
|
|
113
|
+
# Last (or only) session for this agent. Its presence row was just
|
|
114
|
+
# removed, so Matcher stops dispatching to it before task cleanup.
|
|
121
115
|
end
|
|
122
116
|
end
|
|
123
117
|
|
|
@@ -133,13 +127,13 @@ module Collavre
|
|
|
133
127
|
# Fail any tasks still delegated to this agent — including dispatches
|
|
134
128
|
# routed to *work* topics outside the registration inbox. With no live
|
|
135
129
|
# session left, agent_id uniquely scopes this agent's delegated work;
|
|
136
|
-
# scoping by topic_id alone would leak the common case (a
|
|
137
|
-
#
|
|
130
|
+
# scoping by topic_id alone would leak the common case (a dispatch on a
|
|
131
|
+
# project topic) until stuck detection times out.
|
|
138
132
|
cancel_delegated_tasks_for_session(ai_user)
|
|
139
133
|
elsif topic
|
|
140
134
|
# One shared agent fans out to many concurrent sessions (the default
|
|
141
135
|
# AGENT_NAME case). Another session is still LIVE, so agent-wide
|
|
142
|
-
# cleanup would
|
|
136
|
+
# cleanup would cancel the
|
|
143
137
|
# sibling's in-flight work — exactly the hazard AgentChannel#unsubscribed
|
|
144
138
|
# guards against via presence. Tear down ONLY this ending session:
|
|
145
139
|
# cancel work on its own topic (whose client is gone, so it would
|
|
@@ -162,55 +156,13 @@ module Collavre
|
|
|
162
156
|
return
|
|
163
157
|
end
|
|
164
158
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
render json: { error: "Not authorized" }, status: :forbidden
|
|
173
|
-
return
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
agent = resolve_reply_agent(topic, params[:task_id])
|
|
177
|
-
unless agent
|
|
178
|
-
render json: { error: "Not authorized" }, status: :forbidden
|
|
179
|
-
return
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Atomically claim the delegated task BEFORE saving the reply.
|
|
183
|
-
# Two concurrent /reply requests with the same task_id can both
|
|
184
|
-
# pass resolve_reply_agent (which is a read-only scope check) and,
|
|
185
|
-
# without an atomic WHERE status='delegated' transition, both would
|
|
186
|
-
# save separate comments and both would run completion logic —
|
|
187
|
-
# producing duplicate linked replies for one dispatch. Claim first,
|
|
188
|
-
# save second, so the loser sees rows_updated == 0 and bails out
|
|
189
|
-
# before touching the comments table.
|
|
190
|
-
claimed_task = claim_delegated_task(agent, topic, params[:task_id])
|
|
191
|
-
if params[:task_id].present? && claimed_task.nil?
|
|
192
|
-
render json: { error: "Task already completed or not delegated" }, status: :conflict
|
|
193
|
-
return
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
comment = creative.comments.build(
|
|
197
|
-
content: params[:text].to_s,
|
|
198
|
-
topic: topic,
|
|
199
|
-
user: agent,
|
|
200
|
-
skip_default_user: true,
|
|
201
|
-
skip_dispatch: true
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
if comment.save
|
|
205
|
-
task_claim_service.finalize(agent: agent, task: claimed_task, comment: comment) if claimed_task
|
|
206
|
-
dispatch_a2a(agent, comment)
|
|
207
|
-
render json: { comment_id: comment.id }, status: :created
|
|
208
|
-
else
|
|
209
|
-
# Restore the dispatch so the MCP client can retry — the failure
|
|
210
|
-
# is text-level (validation), not task-level.
|
|
211
|
-
claimed_task&.update!(status: "delegated")
|
|
212
|
-
render json: { errors: comment.errors.full_messages }, status: :unprocessable_entity
|
|
213
|
-
end
|
|
159
|
+
result = AiAgent::TaskReplyService.new(
|
|
160
|
+
topic: topic, current_user: current_user, text: params[:text], requested_task_id: params[:task_id],
|
|
161
|
+
agent_resolver: method(:resolve_reply_agent), task_claimer: method(:claim_delegated_task),
|
|
162
|
+
claim_service: task_claim_service
|
|
163
|
+
).call
|
|
164
|
+
dispatch_a2a(result.agent, result.comment.reload, task: result.task) if result.comment
|
|
165
|
+
render json: result.body, status: result.status
|
|
214
166
|
end
|
|
215
167
|
|
|
216
168
|
# POST /api/v1/agent/notify
|
|
@@ -389,7 +341,7 @@ module Collavre
|
|
|
389
341
|
# Mirrors resolve_reply_agent's task_id-first resolution but never
|
|
390
342
|
# touches the task graph (notify only authorizes, it does not complete).
|
|
391
343
|
# A native permission prompt can be raised during a dispatch that
|
|
392
|
-
# selected this session
|
|
344
|
+
# selected this live session on a *work* topic whose
|
|
393
345
|
# primary_agent is unset or a different agent (the case documented on
|
|
394
346
|
# resolve_reply_agent). The echoed active-dispatch task_id authorizes
|
|
395
347
|
# the dispatched session agent directly; without it the topic-
|
|
@@ -419,7 +371,7 @@ module Collavre
|
|
|
419
371
|
# Identify which Claude Channel agent this reply is for.
|
|
420
372
|
#
|
|
421
373
|
# Prefer the echoed task_id from the dispatch payload: the matcher can
|
|
422
|
-
# route to a Claude Channel agent
|
|
374
|
+
# route to a live Claude Channel agent on a topic
|
|
423
375
|
# whose primary_agent is unset or a different agent (e.g. multiple AI
|
|
424
376
|
# agents share a topic, or this agent only has feedback permission on
|
|
425
377
|
# the creative without being the topic's primary). In those cases the
|
|
@@ -495,20 +447,9 @@ module Collavre
|
|
|
495
447
|
|
|
496
448
|
tracker = Orchestration::ResourceTracker.for(agent)
|
|
497
449
|
tasks.find_each do |task|
|
|
498
|
-
task.
|
|
499
|
-
tracker.release!(task.id)
|
|
450
|
+
next unless task.cancel_if_active!(statuses: %w[delegated], pending_tool_call: nil)
|
|
500
451
|
|
|
501
|
-
|
|
502
|
-
begin
|
|
503
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
504
|
-
task, error_message: "Claude Channel session unregistered before reply"
|
|
505
|
-
)
|
|
506
|
-
rescue StandardError => e
|
|
507
|
-
Rails.logger.error(
|
|
508
|
-
"[AgentsController] fail_subtask! failed for task #{task.id}: #{e.message}"
|
|
509
|
-
)
|
|
510
|
-
end
|
|
511
|
-
end
|
|
452
|
+
tracker.release!(task.id)
|
|
512
453
|
|
|
513
454
|
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
514
455
|
end
|
|
@@ -526,8 +467,6 @@ module Collavre
|
|
|
526
467
|
# Release the tracker slot defensively for running tasks — release! is
|
|
527
468
|
# idempotent (Set#delete no-ops on missing key) and we don't know from
|
|
528
469
|
# the DB whether AiAgentJob had already reached tracker.reserve!.
|
|
529
|
-
# Parent subtasks are still failed so workflow executors don't deadlock
|
|
530
|
-
# waiting for a reply that will never come.
|
|
531
470
|
def cancel_pending_tasks_for_session(agent)
|
|
532
471
|
cancel_pending_tasks(Task.where(agent_id: agent.id, status: %w[queued pending running]), agent)
|
|
533
472
|
end
|
|
@@ -538,22 +477,12 @@ module Collavre
|
|
|
538
477
|
tracker = Orchestration::ResourceTracker.for(agent)
|
|
539
478
|
drained_topics = {}
|
|
540
479
|
tasks.find_each do |task|
|
|
541
|
-
|
|
542
|
-
|
|
480
|
+
previous_status = task.cancel_if_active!(statuses: %w[queued pending running])
|
|
481
|
+
next unless previous_status
|
|
482
|
+
|
|
483
|
+
was_running = previous_status == "running"
|
|
543
484
|
tracker.release!(task.id) if was_running
|
|
544
485
|
drained_topics[task.topic_id] ||= task.creative_id
|
|
545
|
-
|
|
546
|
-
if task.parent_task_id.present?
|
|
547
|
-
begin
|
|
548
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
549
|
-
task, error_message: "Claude Channel session unregistered before dispatch"
|
|
550
|
-
)
|
|
551
|
-
rescue StandardError => e
|
|
552
|
-
Rails.logger.error(
|
|
553
|
-
"[AgentsController] fail_subtask! failed for queued task #{task.id}: #{e.message}"
|
|
554
|
-
)
|
|
555
|
-
end
|
|
556
|
-
end
|
|
557
486
|
end
|
|
558
487
|
|
|
559
488
|
# Drain each affected topic's queue once, AFTER all of this session's
|
|
@@ -570,17 +499,36 @@ module Collavre
|
|
|
570
499
|
end
|
|
571
500
|
end
|
|
572
501
|
|
|
573
|
-
def dispatch_a2a(agent, comment)
|
|
502
|
+
def dispatch_a2a(agent, comment, task:)
|
|
574
503
|
AiAgent::A2aDispatcher.new(
|
|
575
504
|
agent: agent,
|
|
576
505
|
reply_comment: comment,
|
|
577
|
-
context: {
|
|
578
|
-
"
|
|
579
|
-
|
|
580
|
-
}
|
|
506
|
+
context: { "creative" => { "id" => comment.creative_id },
|
|
507
|
+
"topic" => { "id" => comment.topic_id },
|
|
508
|
+
SystemEvents::Envelope::KEY => task&.trigger_event_payload&.dig(SystemEvents::Envelope::KEY)
|
|
509
|
+
}.compact,
|
|
510
|
+
workspace_user: workspace_user_for(task)
|
|
581
511
|
).dispatch
|
|
582
512
|
end
|
|
583
513
|
|
|
514
|
+
def workspace_user_for(task)
|
|
515
|
+
unless task
|
|
516
|
+
return current_user unless current_user.ai_user?
|
|
517
|
+
|
|
518
|
+
return
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
payload = task.trigger_event_payload || {}
|
|
522
|
+
user =
|
|
523
|
+
if payload.key?("workspace_user_id")
|
|
524
|
+
User.find_by(id: payload["workspace_user_id"])
|
|
525
|
+
else
|
|
526
|
+
Comment.find_by(id: payload.dig("comment", "id"))&.user
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
user unless user&.ai_user?
|
|
530
|
+
end
|
|
531
|
+
|
|
584
532
|
# Provisions the (agent, topic) identities for a Claude Code
|
|
585
533
|
# registration. Memoized per request; behavior extracted verbatim.
|
|
586
534
|
def session_provisioner
|
|
@@ -14,7 +14,8 @@ module Collavre
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
16
|
# If the original request was for "/" and the user is signed in,
|
|
17
|
-
#
|
|
17
|
+
# return them to their last readable Creative when the authenticated home
|
|
18
|
+
# page is the Creative index; otherwise honor the configured home page.
|
|
18
19
|
# The middleware preserves "/" as a stable URL for unauthenticated
|
|
19
20
|
# visitors; authenticated users get a real URL change so the address
|
|
20
21
|
# bar reflects state.
|
|
@@ -31,13 +32,23 @@ module Collavre
|
|
|
31
32
|
return unless request.env["collavre.root_request"]
|
|
32
33
|
return unless authenticated?
|
|
33
34
|
|
|
34
|
-
target = SystemSetting.home_page_path_authenticated
|
|
35
|
+
target = last_visited_creative_path || SystemSetting.home_page_path_authenticated
|
|
35
36
|
return if target.blank?
|
|
36
37
|
return if target == "/"
|
|
37
38
|
|
|
38
39
|
redirect_to target
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
def last_visited_creative_path
|
|
43
|
+
home_path = HomePagePath.mount_relative(script_name: request.script_name)
|
|
44
|
+
return unless home_path == "/creatives" || home_path == "/creatives.html"
|
|
45
|
+
|
|
46
|
+
creative = Current.user.last_visited_creative
|
|
47
|
+
return unless creative&.has_permission?(Current.user, :read)
|
|
48
|
+
|
|
49
|
+
collavre_engine.creatives_path(id: creative.id, script_name: request.script_name)
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
def set_csrf_token_header
|
|
42
53
|
return unless protect_against_forgery?
|
|
43
54
|
|
|
@@ -1,68 +1,97 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CommentReadPointersController < ApplicationController
|
|
3
|
+
LEGACY_TOPIC_WATERMARK_KEY = "_legacy"
|
|
4
|
+
|
|
3
5
|
def update
|
|
4
6
|
creative = Creative.find(params[:creative_id]).effective_origin
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
topic = params[:topic_id].present? && creative.topics.find_by(id: params[:topic_id])
|
|
8
|
+
return head :unprocessable_entity if params[:topic_id].present? && !topic
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
last_ids = topic ? topic_last_id(creative, topic) : all_message_last_ids(creative)
|
|
11
|
+
positions = Comments::ReadPointerWriter.new(creative: creative, user: Current.user).call(last_ids)
|
|
12
|
+
Comments::ReadReceiptBroadcaster.new(creative: creative).call(positions)
|
|
11
13
|
|
|
12
|
-
mark_inbox_items_read(creative, last_id)
|
|
13
14
|
Comment.broadcast_badge(creative, Current.user)
|
|
14
15
|
|
|
15
|
-
if previous_last_read_id && previous_last_read_id != last_id
|
|
16
|
-
broadcast_read_receipts(creative, previous_last_read_id)
|
|
17
|
-
end
|
|
18
|
-
broadcast_read_receipts(creative, last_id)
|
|
19
|
-
|
|
20
16
|
render json: { success: true }
|
|
17
|
+
rescue Comments::ReadPointerWriter::StaleTopicError
|
|
18
|
+
head :unprocessable_entity
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
private
|
|
24
22
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# We map to the nearest PUBLIC comment to avoid leaking the existence/ID of private comments
|
|
29
|
-
# via the public action cable channel.
|
|
30
|
-
# Trade-off: Private-only threads (with no preceding public comment) will not get real-time read updates.
|
|
31
|
-
effective_id = find_nearest_public_comment_id(creative, comment_id)
|
|
32
|
-
return unless effective_id
|
|
23
|
+
def topic_last_id(creative, topic)
|
|
24
|
+
{ topic.id => creative.comments.visible_to(Current.user).where(topic: topic).maximum(:id) }
|
|
25
|
+
end
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
# All Messages renders Main plus active topics, not archived ones. Do not
|
|
28
|
+
# advance the retained legacy pointer here: an archived topic without its
|
|
29
|
+
# own pointer falls back to it, so moving that watermark would silently mark
|
|
30
|
+
# its hidden comments as read.
|
|
31
|
+
def all_message_last_ids(creative)
|
|
32
|
+
visible_comments = creative.comments.visible_to(Current.user)
|
|
33
|
+
topic_watermarks, legacy_watermark = normalized_topic_watermarks(params[:topic_watermarks])
|
|
34
|
+
return watermarked_last_ids(creative, visible_comments, topic_watermarks, legacy_watermark) if
|
|
35
|
+
topic_watermarks.any? || legacy_watermark.positive?
|
|
36
|
+
|
|
37
|
+
unwatermarked_last_ids(creative, visible_comments)
|
|
38
|
+
end
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
def normalized_topic_watermarks(rendered_topic_watermarks)
|
|
41
|
+
raw_watermarks = rendered_topic_watermarks.respond_to?(:to_unsafe_h) ? rendered_topic_watermarks.to_unsafe_h : rendered_topic_watermarks.to_h
|
|
42
|
+
topic_watermarks = raw_watermarks.filter_map do |topic_id, comment_id|
|
|
43
|
+
[ topic_id.to_i, comment_id.to_i ] if topic_id.to_i.positive? && comment_id.to_i.positive?
|
|
44
|
+
end.to_h
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
[ creative, :comments ],
|
|
40
|
-
target: "read_receipts_comment_#{effective_id}",
|
|
41
|
-
partial: "collavre/comments/read_receipts",
|
|
42
|
-
locals: { read_by_users: users, present_user_ids: present_user_ids }
|
|
43
|
-
)
|
|
46
|
+
[ topic_watermarks, raw_watermarks[LEGACY_TOPIC_WATERMARK_KEY].to_i ]
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
def
|
|
47
|
-
|
|
49
|
+
def watermarked_last_ids(creative, visible_comments, topic_watermarks, legacy_watermark)
|
|
50
|
+
last_ids = bounded_topic_last_ids(creative, visible_comments, topic_watermarks)
|
|
51
|
+
return last_ids unless legacy_watermark.positive?
|
|
52
|
+
|
|
53
|
+
legacy_last_id = visible_comments.where(topic_id: nil).where("comments.id <= ?", legacy_watermark).maximum(:id)
|
|
54
|
+
legacy_last_id ? last_ids.merge(nil => legacy_last_id) : last_ids
|
|
48
55
|
end
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
# Every rendered topic carries its own bound, so the maxima are OR-ed into a
|
|
58
|
+
# single grouped query rather than one MAX per topic. Built from relations
|
|
59
|
+
# rather than a SQL fragment: a hand-built string here would be safe (literal
|
|
60
|
+
# template, bound values) but unprovably so to a scanner.
|
|
61
|
+
def bounded_topic_last_ids(creative, visible_comments, topic_watermarks)
|
|
62
|
+
topic_ids = creative.topics.where(id: topic_watermarks.keys).pluck(:id)
|
|
63
|
+
return {} if topic_ids.empty?
|
|
64
|
+
|
|
65
|
+
scopes = topic_ids.map do |topic_id|
|
|
66
|
+
Comment.where(creative_id: creative.id, topic_id: topic_id)
|
|
67
|
+
.where(Comment.arel_table[:id].lteq(topic_watermarks.fetch(topic_id)))
|
|
68
|
+
end
|
|
69
|
+
maxima = grouped_topic_maxima(visible_comments, scopes)
|
|
70
|
+
topic_ids.index_with { |topic_id| maxima[topic_id] }
|
|
71
|
+
end
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
def unwatermarked_last_ids(creative, visible_comments)
|
|
74
|
+
rendered_topic_ids_supplied = params.key?(:topic_ids)
|
|
75
|
+
topics = rendered_topic_ids_supplied ? creative.topics.where(id: Array(params[:topic_ids])) : creative.topics
|
|
76
|
+
topic_ids = topics.pluck(:id)
|
|
77
|
+
maxima = visible_comments.where(topic_id: topic_ids).group(:topic_id).maximum(:id)
|
|
78
|
+
last_ids = topic_ids.index_with { |topic_id| maxima[topic_id] }
|
|
79
|
+
return last_ids if rendered_topic_ids_supplied
|
|
55
80
|
|
|
56
|
-
|
|
81
|
+
legacy_client_last_ids(visible_comments, last_ids)
|
|
82
|
+
end
|
|
57
83
|
|
|
58
|
-
|
|
84
|
+
# Clients deployed before topic-scoped watermarks only send creative_id.
|
|
85
|
+
# Preserve the previous creative-wide endpoint semantics for them: every
|
|
86
|
+
# named topic, including archived topics, and the retained legacy lane
|
|
87
|
+
# advance through the visible history.
|
|
88
|
+
def legacy_client_last_ids(visible_comments, last_ids)
|
|
89
|
+
legacy_last_id = visible_comments.where(topic_id: nil).maximum(:id)
|
|
90
|
+
legacy_last_id ? last_ids.merge(nil => legacy_last_id) : last_ids
|
|
59
91
|
end
|
|
60
92
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# automatically handles the unread count for the inbox badge.
|
|
64
|
-
def mark_inbox_items_read(_creative, _last_comment_id)
|
|
65
|
-
# Intentionally empty — kept for backward compatibility during transition.
|
|
93
|
+
def grouped_topic_maxima(visible_comments, scopes)
|
|
94
|
+
visible_comments.merge(scopes.reduce { |combined, scope| combined.or(scope) }).group(:topic_id).maximum(:id)
|
|
66
95
|
end
|
|
67
96
|
end
|
|
68
97
|
end
|