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,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module SystemEvents
|
|
5
|
+
# The registry of event names orchestration can dispatch.
|
|
6
|
+
module Vocabulary
|
|
7
|
+
UnknownEvent = Class.new(ArgumentError)
|
|
8
|
+
Definition = Data.define(:name, :required_keys, :sources)
|
|
9
|
+
|
|
10
|
+
EVENTS = {
|
|
11
|
+
"comment_created" => Definition.new(
|
|
12
|
+
name: "comment_created",
|
|
13
|
+
required_keys: %w[comment creative],
|
|
14
|
+
sources: %w[comment_callback cron a2a drop_trigger trigger_restart]
|
|
15
|
+
)
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def self.names
|
|
19
|
+
EVENTS.keys
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.known?(name)
|
|
23
|
+
EVENTS.key?(name.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.fetch(name)
|
|
27
|
+
EVENTS.fetch(name.to_s) do
|
|
28
|
+
raise UnknownEvent,
|
|
29
|
+
"Unknown system event #{name.inspect}. Known events: #{names.join(', ')}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Required keys are advisory. The dispatcher warns rather than refusing
|
|
34
|
+
# a malformed payload, preserving the behavior of existing callers.
|
|
35
|
+
def self.missing_keys(name, payload)
|
|
36
|
+
return [] unless known?(name)
|
|
37
|
+
|
|
38
|
+
payload ||= {}
|
|
39
|
+
fetch(name).required_keys.reject do |key|
|
|
40
|
+
!payload[key].nil? || !payload[key.to_sym].nil?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -177,7 +177,7 @@ module Tools
|
|
|
177
177
|
if include_comments
|
|
178
178
|
result[:recent_comments] = creative.comments.order(created_at: :desc).limit(3).map do |comment|
|
|
179
179
|
{
|
|
180
|
-
content:
|
|
180
|
+
content: Collavre::HtmlText.plain(comment.content).strip.truncate(200),
|
|
181
181
|
user: comment.user&.display_name || comment.user&.name,
|
|
182
182
|
created_at: comment.created_at&.iso8601
|
|
183
183
|
}
|
|
@@ -33,8 +33,8 @@ module Tools
|
|
|
33
33
|
return { error: "No write permission on this Creative", id: creative_id }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
return
|
|
36
|
+
topic = resolve_topic(creative, topic_name)
|
|
37
|
+
return topic if topic.is_a?(Hash) && topic[:error]
|
|
38
38
|
|
|
39
39
|
parsed = Fugit.parse(schedule)
|
|
40
40
|
unless parsed.is_a?(Fugit::Cron)
|
|
@@ -44,20 +44,11 @@ module Tools
|
|
|
44
44
|
suffix = SecureRandom.hex(4)
|
|
45
45
|
key = "cron_#{creative_id}_#{suffix}"
|
|
46
46
|
|
|
47
|
-
task =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
schedule: schedule,
|
|
51
|
-
queue_name: "default",
|
|
52
|
-
static: false,
|
|
53
|
-
description: description || "Cron job for creative #{creative_id}",
|
|
54
|
-
arguments: [ {
|
|
55
|
-
creative_id: creative_id,
|
|
56
|
-
topic_id: topic_id,
|
|
57
|
-
agent_id: Current.user.id,
|
|
58
|
-
message: message
|
|
59
|
-
} ]
|
|
47
|
+
task = create_task(
|
|
48
|
+
topic: topic, creative: creative, creative_id: creative_id,
|
|
49
|
+
key: key, schedule: schedule, message: message, description: description
|
|
60
50
|
)
|
|
51
|
+
return task if task.is_a?(Hash) && task[:error]
|
|
61
52
|
|
|
62
53
|
{
|
|
63
54
|
success: true,
|
|
@@ -72,15 +63,38 @@ module Tools
|
|
|
72
63
|
|
|
73
64
|
private
|
|
74
65
|
|
|
75
|
-
def
|
|
66
|
+
def create_task(topic:, creative:, creative_id:, key:, schedule:, message:, description:)
|
|
67
|
+
topic.with_lock do
|
|
68
|
+
unless topic.creative_id == creative.effective_origin.id
|
|
69
|
+
return { error: I18n.t("collavre.comments.invalid_topic") }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
SolidQueue::RecurringTask.create!(
|
|
73
|
+
key: key,
|
|
74
|
+
class_name: "Collavre::CronActionJob",
|
|
75
|
+
schedule: schedule,
|
|
76
|
+
queue_name: "default",
|
|
77
|
+
static: false,
|
|
78
|
+
description: description || "Cron job for creative #{creative_id}",
|
|
79
|
+
arguments: [ {
|
|
80
|
+
creative_id: creative_id,
|
|
81
|
+
topic_id: topic.id,
|
|
82
|
+
agent_id: Current.user.id,
|
|
83
|
+
message: message
|
|
84
|
+
} ]
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolve_topic(creative, topic_name)
|
|
76
90
|
origin = creative.effective_origin
|
|
77
91
|
if topic_name.casecmp(Creative::MAIN_TOPIC_NAME).zero?
|
|
78
|
-
origin.main_topic(fallback_user: Current.user)
|
|
92
|
+
origin.main_topic(fallback_user: Current.user)
|
|
79
93
|
else
|
|
80
94
|
topic = Topic.find_by(name: topic_name, creative_id: origin.id)
|
|
81
95
|
return { error: "Topic '#{topic_name}' not found for this creative" } unless topic
|
|
82
96
|
|
|
83
|
-
topic
|
|
97
|
+
topic
|
|
84
98
|
end
|
|
85
99
|
end
|
|
86
100
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Tools
|
|
5
|
+
# Parses the "list of ids" parameters the topic tools take.
|
|
6
|
+
#
|
|
7
|
+
# MCP schemas describe a list, but what actually arrives depends on the
|
|
8
|
+
# calling model: "12,45", ["12", 45], 12 and "12" are all things an agent
|
|
9
|
+
# writes when it means the same thing. Rejecting four fifths of those is a
|
|
10
|
+
# tool that fails for a reason the caller cannot see in its own request, so
|
|
11
|
+
# every shape is accepted here instead of at four separate call sites.
|
|
12
|
+
#
|
|
13
|
+
# Lenient about shape, strict about content. A token that is not entirely
|
|
14
|
+
# digits raises rather than coercing: to_i turns "12.5" into 12 and
|
|
15
|
+
# "123oops" into 123, and topic_create/topic_branch would then move a
|
|
16
|
+
# different message than the one named and report success. A tool that
|
|
17
|
+
# silently acts on a neighbouring id is worse than one that fails.
|
|
18
|
+
module IdList
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
DIGITS = /\A\d+\z/
|
|
22
|
+
|
|
23
|
+
def parse(value)
|
|
24
|
+
Array(value)
|
|
25
|
+
.flat_map { |entry| entry.to_s.split(",") }
|
|
26
|
+
.filter_map { |token| token.strip.presence }
|
|
27
|
+
.map { |token| cast(token) }
|
|
28
|
+
.uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cast(token)
|
|
32
|
+
raise ArgumentError, "#{token.inspect} is not a valid id — ids must be whole numbers" unless token.match?(DIGITS)
|
|
33
|
+
|
|
34
|
+
token.to_i
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -2,22 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
module Collavre
|
|
4
4
|
module Tools
|
|
5
|
-
# Topic
|
|
6
|
-
# CreativePermissionGuard
|
|
7
|
-
# controller request.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# Topic authorization for MCP tool services. Mirrors
|
|
6
|
+
# CreativePermissionGuard's require_creative_{read,write,admin}! but is
|
|
7
|
+
# callable outside a controller request. Every check resolves the topic's
|
|
8
|
+
# effective_origin creative first, because that is where shares live — a
|
|
9
|
+
# linked creative carries none of its own.
|
|
10
|
+
#
|
|
11
|
+
# The owner short-circuit matches CreativePermissionGuard: an owner can hold
|
|
12
|
+
# no explicit share on their own creative, so has_permission? alone would
|
|
13
|
+
# lock them out of their own topics.
|
|
10
14
|
module TopicAuthorizer
|
|
11
15
|
module_function
|
|
12
16
|
|
|
17
|
+
# Reading a topic's messages exposes every participant's words, so it is
|
|
18
|
+
# gated at :read — the same level the creative's own body needs.
|
|
19
|
+
def authorize_read!(topic, user: Collavre::Current.user)
|
|
20
|
+
authorize!(topic, :read, user: user)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Posting a message is the topic equivalent of commenting through
|
|
24
|
+
# CommentsController, so it uses the same feedback permission floor.
|
|
25
|
+
def authorize_feedback!(topic, user: Collavre::Current.user)
|
|
26
|
+
authorize!(topic, :feedback, user: user)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Attaching a channel injects external messages into a topic, and creating
|
|
30
|
+
# or archiving one restructures where conversation lands, so both are
|
|
31
|
+
# write-equivalent mutations.
|
|
13
32
|
def authorize_write!(topic, user: Collavre::Current.user)
|
|
33
|
+
authorize!(topic, :write, user: user)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Renaming a topic rewrites a name other members' links and habits point
|
|
37
|
+
# at, so TopicsController gates it at :admin. Tools must not be the softer
|
|
38
|
+
# door onto the same mutation.
|
|
39
|
+
def authorize_admin!(topic, user: Collavre::Current.user)
|
|
40
|
+
authorize!(topic, :admin, user: user)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Topic creation has no topic to authorize against yet, so the creative is
|
|
44
|
+
# the subject. Kept here so every topic-shaped permission decision reads
|
|
45
|
+
# from one place.
|
|
46
|
+
def authorize_creative!(creative, level, user: Collavre::Current.user)
|
|
47
|
+
origin = creative&.effective_origin
|
|
48
|
+
raise ArgumentError, "Creative is required" unless origin
|
|
49
|
+
return if origin.user == user
|
|
50
|
+
return if user && origin.has_permission?(user, level)
|
|
51
|
+
|
|
52
|
+
raise Collavre::Tools::PermissionDeniedError,
|
|
53
|
+
"No #{level} permission on creative #{origin.id}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def authorize!(topic, level, user: Collavre::Current.user)
|
|
14
57
|
creative = topic.creative&.effective_origin
|
|
15
58
|
raise ArgumentError, "Topic has no creative" unless creative
|
|
16
59
|
return if creative.user == user
|
|
17
|
-
return if user && creative.has_permission?(user,
|
|
60
|
+
return if user && creative.has_permission?(user, level)
|
|
18
61
|
|
|
19
62
|
raise Collavre::Tools::PermissionDeniedError,
|
|
20
|
-
"No
|
|
63
|
+
"No #{level} permission on topic #{topic.id}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# True when the user may read the topic. Used by the multi-topic read
|
|
67
|
+
# tools, which report an inaccessible topic as a per-topic error entry
|
|
68
|
+
# rather than failing the whole call — one unreadable id in a batch of
|
|
69
|
+
# three should not discard the two that worked.
|
|
70
|
+
def readable?(topic, user: Collavre::Current.user)
|
|
71
|
+
authorize_read!(topic, user: user)
|
|
72
|
+
true
|
|
73
|
+
rescue Collavre::Tools::PermissionDeniedError, ArgumentError
|
|
74
|
+
false
|
|
21
75
|
end
|
|
22
76
|
end
|
|
23
77
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
require "sorbet-runtime"
|
|
3
|
+
require "rails_mcp_engine"
|
|
4
|
+
module Tools
|
|
5
|
+
# Branches selected messages of a topic into a new topic, copying them.
|
|
6
|
+
#
|
|
7
|
+
# The context-length escape hatch. A topic that has run long is expensive for
|
|
8
|
+
# every agent that reads it and hard for a person to re-enter, but the history
|
|
9
|
+
# is not disposable. Branching carries the few messages the next stretch of
|
|
10
|
+
# work actually depends on into a fresh topic and leaves the original intact
|
|
11
|
+
# as the record.
|
|
12
|
+
class TopicBranchService
|
|
13
|
+
extend T::Sig
|
|
14
|
+
extend ToolMeta
|
|
15
|
+
|
|
16
|
+
tool_name "topic_branch"
|
|
17
|
+
tool_description <<~DESC.strip
|
|
18
|
+
Branch a topic: create a new topic containing COPIES of selected messages
|
|
19
|
+
from an existing one. The originals stay where they are.
|
|
20
|
+
|
|
21
|
+
Use this to reset a conversation that has grown too long to carry, or to
|
|
22
|
+
split one thread that has drifted into two subjects, while keeping the
|
|
23
|
+
source topic as the record. To move messages instead of copying them, use
|
|
24
|
+
topic_create with comment_ids.
|
|
25
|
+
|
|
26
|
+
Get the message ids from topic_messages. At most
|
|
27
|
+
#{::Collavre::TopicBranchService::MAX_BRANCH_COMMENTS} messages per branch
|
|
28
|
+
— pick the ones the next stretch of work depends on rather than the whole
|
|
29
|
+
history. Asking for more is an error, not a silent trim.
|
|
30
|
+
|
|
31
|
+
The new topic is created on the same creative and records the source in
|
|
32
|
+
source_topic_id. Requires feedback permission or better on the creative.
|
|
33
|
+
Main and an inbox's System name are reserved and cannot be assigned to
|
|
34
|
+
the branch.
|
|
35
|
+
|
|
36
|
+
Approval prompts cannot be branched — a copy would lose the approval
|
|
37
|
+
action and read as ordinary history. Passing one is an error.
|
|
38
|
+
DESC
|
|
39
|
+
|
|
40
|
+
tool_param :source_topic_id, description: "The topic to branch from."
|
|
41
|
+
tool_param :comment_ids, description: "Comma-separated ids of the messages to copy, e.g. \"991,994,1002\". They must all belong to the source topic and be visible to you. Get ids from topic_messages."
|
|
42
|
+
tool_param :name, description: "Name for the new topic. Defaults to a generated \"branch:<source name>\" name.", required: false
|
|
43
|
+
|
|
44
|
+
sig do
|
|
45
|
+
params(
|
|
46
|
+
source_topic_id: Integer,
|
|
47
|
+
comment_ids: T.any(String, Integer, T::Array[T.untyped]),
|
|
48
|
+
name: T.nilable(String)
|
|
49
|
+
).returns(T::Hash[Symbol, T.untyped])
|
|
50
|
+
end
|
|
51
|
+
def call(source_topic_id:, comment_ids:, name: nil)
|
|
52
|
+
user = Current.user || raise("Current.user is required")
|
|
53
|
+
source = Topic.find(source_topic_id)
|
|
54
|
+
TopicAuthorizer.authorize_read!(source, user: user)
|
|
55
|
+
|
|
56
|
+
ids = IdList.parse(comment_ids)
|
|
57
|
+
validate_ids!(ids)
|
|
58
|
+
|
|
59
|
+
# Fully qualified: inside Collavre::Tools, a bare TopicBranchService is
|
|
60
|
+
# this tool, not the branching service it wraps.
|
|
61
|
+
topic = ::Collavre::TopicBranchService.new(
|
|
62
|
+
creative: source.creative, user: user, source_topic: source, name: name
|
|
63
|
+
).call(comment_ids: ids)
|
|
64
|
+
|
|
65
|
+
Topics::Serializer.for_tool(topic).merge(source_topic_id: source.id, copied_count: ids.size)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# TopicBranchService trims an oversized selection to its cap and reports
|
|
71
|
+
# success for the shortened list, which is right for a UI that already
|
|
72
|
+
# capped the selection but wrong here: a caller that pasted 150 ids would be
|
|
73
|
+
# told it branched them and be missing 50 with nothing to read that says so.
|
|
74
|
+
def validate_ids!(ids)
|
|
75
|
+
cap = ::Collavre::TopicBranchService::MAX_BRANCH_COMMENTS
|
|
76
|
+
raise ArgumentError, "comment_ids is required" if ids.empty?
|
|
77
|
+
return if ids.size <= cap
|
|
78
|
+
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"#{ids.size} messages requested but a branch copies at most #{cap}. " \
|
|
81
|
+
"Select fewer messages, or branch more than once."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
require "sorbet-runtime"
|
|
3
|
+
require "rails_mcp_engine"
|
|
4
|
+
module Tools
|
|
5
|
+
# Creates a topic on a creative, optionally pinning a primary agent and
|
|
6
|
+
# optionally moving existing messages into it.
|
|
7
|
+
#
|
|
8
|
+
# This is the fan-out primitive. Topics are the concurrency unit — tasks in
|
|
9
|
+
# one topic queue behind each other, tasks in different topics run in parallel
|
|
10
|
+
# — so an agent that can only create creatives can split work up on paper but
|
|
11
|
+
# cannot actually run it in parallel. Creating a topic per unit of work, with
|
|
12
|
+
# the agent that owns it pinned, is what turns a plan into concurrent work.
|
|
13
|
+
class TopicCreateService
|
|
14
|
+
extend T::Sig
|
|
15
|
+
extend ToolMeta
|
|
16
|
+
|
|
17
|
+
tool_name "topic_create"
|
|
18
|
+
tool_description <<~DESC.strip
|
|
19
|
+
Create a topic (conversation thread) on a creative.
|
|
20
|
+
|
|
21
|
+
Topics are how work runs in parallel in Collavre: tasks dispatched in the
|
|
22
|
+
same topic are serialized — one holds the topic's slot while the rest
|
|
23
|
+
queue — while tasks in different topics run concurrently. Split work into
|
|
24
|
+
one topic per independently-runnable unit; do not pile parallel work into
|
|
25
|
+
one topic expecting it to overlap.
|
|
26
|
+
|
|
27
|
+
primary_agent pins one agent to the topic. A pin is EXCLUSIVE: the pinned
|
|
28
|
+
agent alone answers ambient messages there and no other agent responds, so
|
|
29
|
+
use it for a dedicated 1:1 working channel and leave it unset for a thread
|
|
30
|
+
several participants should hear. The agent must already have feedback
|
|
31
|
+
permission or better on the creative, otherwise the pin is refused (a
|
|
32
|
+
pinned agent that cannot answer would silence the topic for everyone).
|
|
33
|
+
|
|
34
|
+
Accepts an agent id, email, or exact name for primary_agent. Searchable
|
|
35
|
+
agents, agents created by the caller, and private agents already shared
|
|
36
|
+
on this creative can be resolved. Omit `name` to get the next default
|
|
37
|
+
name ("Topic 3"). Main and an inbox's System name are reserved and
|
|
38
|
+
cannot be assigned to a new topic.
|
|
39
|
+
|
|
40
|
+
comment_ids moves existing messages out of their current topic into the
|
|
41
|
+
new one. To copy messages and leave the originals in place, use
|
|
42
|
+
topic_branch instead.
|
|
43
|
+
|
|
44
|
+
Requires write permission on the creative.
|
|
45
|
+
DESC
|
|
46
|
+
|
|
47
|
+
tool_param :creative_id, description: "The creative to create the topic on."
|
|
48
|
+
tool_param :name, description: "Topic name. Must be unique on the creative. Defaults to the next generated name, e.g. \"Topic 3\".", required: false
|
|
49
|
+
tool_param :primary_agent, description: "Agent to pin to this topic — id, email, or exact name. The pin is exclusive: only this agent answers here. The agent needs feedback permission or better on the creative.", required: false
|
|
50
|
+
tool_param :comment_ids, description: "Comma-separated ids of existing messages to MOVE into the new topic. Use topic_branch to copy instead of move.", required: false
|
|
51
|
+
|
|
52
|
+
sig do
|
|
53
|
+
params(
|
|
54
|
+
creative_id: Integer,
|
|
55
|
+
name: T.nilable(String),
|
|
56
|
+
primary_agent: T.nilable(String),
|
|
57
|
+
comment_ids: T.nilable(T.any(String, Integer, T::Array[T.untyped]))
|
|
58
|
+
).returns(T::Hash[Symbol, T.untyped])
|
|
59
|
+
end
|
|
60
|
+
def call(creative_id:, name: nil, primary_agent: nil, comment_ids: nil)
|
|
61
|
+
user = Current.user || raise("Current.user is required")
|
|
62
|
+
creative = Creative.find(creative_id).effective_origin
|
|
63
|
+
TopicAuthorizer.authorize_creative!(creative, :write, user: user)
|
|
64
|
+
|
|
65
|
+
agent = resolve_agent(primary_agent, creative)
|
|
66
|
+
topic = build_topic(creative, name, user)
|
|
67
|
+
|
|
68
|
+
Topic.transaction do
|
|
69
|
+
topic.save!
|
|
70
|
+
topic.set_primary_agent!(agent) if agent
|
|
71
|
+
move_comments(creative, user, topic, comment_ids)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
broadcast(creative, topic, agent, user)
|
|
75
|
+
Topics::Serializer.for_tool(topic)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Checked before the topic exists, exactly as TopicsController#create does:
|
|
81
|
+
# a topic created here never carries a session_id, so a Claude Channel
|
|
82
|
+
# session agent can never be routed on it and pinning one would produce a
|
|
83
|
+
# dead topic rather than an error.
|
|
84
|
+
def resolve_agent(primary_agent, creative)
|
|
85
|
+
resolved = Topics::AgentResolver.call(primary_agent, creative: creative)
|
|
86
|
+
return nil if resolved.nil? || resolved == Topics::AgentResolver::CLEAR
|
|
87
|
+
|
|
88
|
+
rejection = Topic.primary_agent_rejection(creative, resolved)
|
|
89
|
+
raise ArgumentError, Topics::PinRejection.message(resolved, rejection) if rejection
|
|
90
|
+
|
|
91
|
+
resolved
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_topic(creative, name, user)
|
|
95
|
+
topic_name = name.presence || Topics::NextName.for(creative)
|
|
96
|
+
Topics::ReservedName.reject!(creative, topic_name)
|
|
97
|
+
creative.topics.build(name: topic_name, user: user)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def move_comments(creative, user, topic, comment_ids)
|
|
101
|
+
ids = IdList.parse(comment_ids)
|
|
102
|
+
return if ids.empty?
|
|
103
|
+
|
|
104
|
+
CommentMoveService.new(creative: creative, user: user).call(comment_ids: ids, target_topic_id: topic.id)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# user_id in the payload is what makes the creating user's own client select
|
|
108
|
+
# the new topic. Included for parity with TopicsController#create: a user
|
|
109
|
+
# asking an agent to open a topic expects to land in it, the same as if they
|
|
110
|
+
# had made it themselves.
|
|
111
|
+
def broadcast(creative, topic, agent, user)
|
|
112
|
+
payload = agent ? Topics::Serializer.with_agent(topic, agent) : topic.slice(:id, :name)
|
|
113
|
+
TopicsChannel.broadcast_to(creative, { action: "created", topic: payload, user_id: user.id })
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
require "sorbet-runtime"
|
|
3
|
+
require "rails_mcp_engine"
|
|
4
|
+
module Tools
|
|
5
|
+
# Lists a creative's topics, or describes specific topics by id.
|
|
6
|
+
#
|
|
7
|
+
# This is the planning call. A topic is a Collavre conversation's concurrency
|
|
8
|
+
# unit — work in separate topics runs in parallel, work piled into one topic
|
|
9
|
+
# queues — so an agent deciding how to split a job needs to see what topics
|
|
10
|
+
# already exist, who is pinned to them, and how much conversation each one
|
|
11
|
+
# holds, before it reads a single message.
|
|
12
|
+
class TopicListService
|
|
13
|
+
extend T::Sig
|
|
14
|
+
extend ToolMeta
|
|
15
|
+
|
|
16
|
+
tool_name "topic_list"
|
|
17
|
+
tool_description <<~DESC.strip
|
|
18
|
+
List the topics of a creative, or describe specific topics by id.
|
|
19
|
+
|
|
20
|
+
A topic is a conversation thread on a creative, and it is also the unit of
|
|
21
|
+
agent concurrency: tasks in different topics run in parallel, tasks in the
|
|
22
|
+
same topic queue behind each other. Use this before splitting work up, and
|
|
23
|
+
before calling topic_messages — the per-topic message_count and
|
|
24
|
+
message_chars tell you how much conversation you would be pulling in, so
|
|
25
|
+
you can pick an offset/limit that fits your context.
|
|
26
|
+
|
|
27
|
+
Pass creative_id to list a creative's topics, or topic_ids to describe
|
|
28
|
+
known topics (which may live on different creatives). Returns, per topic:
|
|
29
|
+
id, name, creative_id, archived, main/system flags, source_topic_id (set
|
|
30
|
+
when the topic was branched off another), primary_agent, agent_locked, and
|
|
31
|
+
the message totals.
|
|
32
|
+
|
|
33
|
+
message_chars combines stored message HTML length with a conservative
|
|
34
|
+
estimate for image attachment markers. It is useful for relative sizing,
|
|
35
|
+
not exact budgeting.
|
|
36
|
+
|
|
37
|
+
Requires read permission on each topic's creative. Unknown or unreadable
|
|
38
|
+
ids come back in "errors" instead of failing the call.
|
|
39
|
+
DESC
|
|
40
|
+
|
|
41
|
+
tool_param :creative_id, description: "List every topic on this creative. Either this or topic_ids is required.", required: false
|
|
42
|
+
tool_param :topic_ids, description: "Describe these specific topics. Comma-separated ids, e.g. \"12,45,78\" (max #{TopicSelection::MAX_TOPICS} per call — asking for more is an error, not a silent trim). Either this or creative_id is required.", required: false
|
|
43
|
+
tool_param :include_archived, description: "Include archived topics when listing by creative_id (default: false). Archived topics keep their messages and stay readable by id.", required: false
|
|
44
|
+
tool_param :include_stats, description: "Include message_count / message_chars / last_message_at (default: true). Pass false to skip the totals query on a creative with very many topics.", required: false
|
|
45
|
+
tool_param :include_system, description: "Count authorless system messages in the totals (default: false). Approval prompts are never counted. Matches the topic_messages default so the counts describe the same set of messages you would read back.", required: false
|
|
46
|
+
|
|
47
|
+
sig do
|
|
48
|
+
params(
|
|
49
|
+
creative_id: T.nilable(Integer),
|
|
50
|
+
topic_ids: T.nilable(T.any(String, Integer, T::Array[T.untyped])),
|
|
51
|
+
include_archived: T.nilable(T::Boolean),
|
|
52
|
+
include_stats: T.nilable(T::Boolean),
|
|
53
|
+
include_system: T.nilable(T::Boolean)
|
|
54
|
+
).returns(T::Hash[Symbol, T.untyped])
|
|
55
|
+
end
|
|
56
|
+
def call(creative_id: nil, topic_ids: nil, include_archived: false, include_stats: true, include_system: false)
|
|
57
|
+
user = require_user!
|
|
58
|
+
include_stats = true if include_stats.nil?
|
|
59
|
+
|
|
60
|
+
topics, errors = select_topics(creative_id, topic_ids, user, include_archived)
|
|
61
|
+
stats = include_stats ? Topics::MessageStats.for(topics, user: user, include_system: include_system) : {}
|
|
62
|
+
|
|
63
|
+
{ topics: topics.map { |topic| describe(topic, stats[topic.id]) }, errors: errors }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def require_user!
|
|
69
|
+
Current.user || raise("Current.user is required")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def select_topics(creative_id, topic_ids, user, include_archived)
|
|
73
|
+
ids = IdList.parse(topic_ids)
|
|
74
|
+
return TopicSelection.resolve(ids, user: user) if ids.any?
|
|
75
|
+
raise ArgumentError, "Either creative_id or topic_ids is required" if creative_id.blank?
|
|
76
|
+
|
|
77
|
+
[ topics_of(creative_id, user, include_archived), [] ]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# effective_origin because topics hang off the origin creative, not off a
|
|
81
|
+
# link to it — passing a linked creative's id would otherwise list nothing.
|
|
82
|
+
def topics_of(creative_id, user, include_archived)
|
|
83
|
+
creative = Creative.find(creative_id).effective_origin
|
|
84
|
+
TopicAuthorizer.authorize_creative!(creative, :read, user: user)
|
|
85
|
+
|
|
86
|
+
scope = creative.topics
|
|
87
|
+
scope = scope.active unless include_archived
|
|
88
|
+
scope.includes(:primary_agent).to_a
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def describe(topic, stat)
|
|
92
|
+
Topics::Serializer.for_tool(
|
|
93
|
+
topic,
|
|
94
|
+
message_count: stat&.count,
|
|
95
|
+
message_chars: stat&.chars,
|
|
96
|
+
last_message_at: stat&.last_at
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|