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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Tools
|
|
5
|
+
# Turns the `topic_ids` parameter of the read tools into topics, and reports
|
|
6
|
+
# the ones it could not turn into topics instead of raising.
|
|
7
|
+
#
|
|
8
|
+
# A caller asking about three topics has usually copied the ids out of a
|
|
9
|
+
# conversation, so one of them being stale or unshared is ordinary. Failing
|
|
10
|
+
# the whole call there would throw away the two that worked and give no way
|
|
11
|
+
# to tell which id was the bad one.
|
|
12
|
+
module TopicSelection
|
|
13
|
+
# A ceiling on topics per call, not on messages — that is max_chars' job.
|
|
14
|
+
# This one exists because each topic costs a header, a totals query and a
|
|
15
|
+
# window query, and a caller that pastes forty ids has stopped planning
|
|
16
|
+
# and started dumping.
|
|
17
|
+
MAX_TOPICS = 20
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Returns [topics, errors], preserving the caller's id order so a numbered
|
|
22
|
+
# request reads back in the order it was asked.
|
|
23
|
+
def resolve(ids, user: Collavre::Current.user)
|
|
24
|
+
enforce_cap!(ids)
|
|
25
|
+
by_id = Topic.where(id: ids).index_by(&:id)
|
|
26
|
+
|
|
27
|
+
ids.each_with_object([ [], [] ]) do |id, (topics, errors)|
|
|
28
|
+
error = rejection_for(by_id[id], user)
|
|
29
|
+
error ? errors << { topic_id: id, error: error } : topics << by_id[id]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# An oversized batch is an error, not a trim. Silently keeping the first
|
|
34
|
+
# twenty returns a response that looks complete — every topic asked for
|
|
35
|
+
# that appears, appears in full — while whole conversations are missing
|
|
36
|
+
# with nothing in the payload that says so, and a caller summarizing
|
|
37
|
+
# "all of these" would report on a subset believing it had them all.
|
|
38
|
+
# Unreadable ids are still reported per topic rather than raised: those
|
|
39
|
+
# the caller can see and act on, one entry each.
|
|
40
|
+
def enforce_cap!(ids)
|
|
41
|
+
return if ids.size <= MAX_TOPICS
|
|
42
|
+
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"#{ids.size} topics requested but at most #{MAX_TOPICS} can be read per call. " \
|
|
45
|
+
"Split the ids across several calls."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Deliberately the same message for missing and unreadable. Distinguishing
|
|
49
|
+
# them would let a caller probe which topic ids exist on creatives they
|
|
50
|
+
# cannot see.
|
|
51
|
+
def rejection_for(topic, user)
|
|
52
|
+
return "Topic not found or not readable" if topic.nil?
|
|
53
|
+
return nil if TopicAuthorizer.readable?(topic, user: user)
|
|
54
|
+
|
|
55
|
+
"Topic not found or not readable"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
require "sorbet-runtime"
|
|
3
|
+
require "rails_mcp_engine"
|
|
4
|
+
module Tools
|
|
5
|
+
# Renames a topic, archives/unarchives it, or changes its primary agent.
|
|
6
|
+
#
|
|
7
|
+
# One tool rather than three because these are the three ways a topic changes
|
|
8
|
+
# after it exists, and they are routinely done together — an agent finishing a
|
|
9
|
+
# unit of work archives the topic and releases the pin in the same breath.
|
|
10
|
+
# They do not share a permission level, so each field is authorized for what
|
|
11
|
+
# it actually does.
|
|
12
|
+
class TopicUpdateService
|
|
13
|
+
extend T::Sig
|
|
14
|
+
extend ToolMeta
|
|
15
|
+
|
|
16
|
+
tool_name "topic_update"
|
|
17
|
+
tool_description <<~DESC.strip
|
|
18
|
+
Rename a topic, archive or unarchive it, or set/clear its primary agent.
|
|
19
|
+
Pass only the fields you want to change.
|
|
20
|
+
|
|
21
|
+
Archiving is how a finished thread is put away: the topic leaves the
|
|
22
|
+
active sidebar but keeps every message, and topic_messages can still read
|
|
23
|
+
it by id. Archive a topic when its conversation has reached its
|
|
24
|
+
conclusion, rather than reusing it for the next subject — a topic is one
|
|
25
|
+
thread of one conclusion, and it is also the concurrency unit, so reusing
|
|
26
|
+
one serializes unrelated work behind it.
|
|
27
|
+
|
|
28
|
+
primary_agent pins one agent to the topic, EXCLUSIVELY: the pinned agent
|
|
29
|
+
alone answers ambient messages and no other agent responds. The agent must
|
|
30
|
+
already have feedback permission or better on the creative. Pass "none" to
|
|
31
|
+
clear the pin and let normal routing resume. Accepts an agent id, email,
|
|
32
|
+
or exact name; private agents already shared on this creative resolve too.
|
|
33
|
+
|
|
34
|
+
Permissions: renaming needs admin on the creative (a rename changes what
|
|
35
|
+
everyone else's links and habits point at); archiving and pin changes need
|
|
36
|
+
write. Main and an inbox's System topic are reserved: their names and
|
|
37
|
+
archive state cannot change, and those names cannot be assigned to another
|
|
38
|
+
topic, but their pin can. A Claude Channel session topic's pin is part of
|
|
39
|
+
its session identity and cannot be changed here.
|
|
40
|
+
DESC
|
|
41
|
+
|
|
42
|
+
tool_param :topic_id, description: "The topic to update."
|
|
43
|
+
tool_param :name, description: "New name. Must be unique on the creative. Requires admin permission.", required: false
|
|
44
|
+
tool_param :archived, description: "true archives the topic (hides it from the active list, keeps all messages); false unarchives it.", required: false
|
|
45
|
+
tool_param :primary_agent, description: "Agent to pin — id, email, or exact name. Pass \"none\" to clear the pin. The pin is exclusive: only this agent answers in the topic.", required: false
|
|
46
|
+
|
|
47
|
+
sig do
|
|
48
|
+
params(
|
|
49
|
+
topic_id: Integer,
|
|
50
|
+
name: T.nilable(String),
|
|
51
|
+
archived: T.nilable(T::Boolean),
|
|
52
|
+
primary_agent: T.nilable(String)
|
|
53
|
+
).returns(T::Hash[Symbol, T.untyped])
|
|
54
|
+
end
|
|
55
|
+
def call(topic_id:, name: nil, archived: nil, primary_agent: nil)
|
|
56
|
+
Current.user || raise("Current.user is required")
|
|
57
|
+
topic = Topic.find(topic_id)
|
|
58
|
+
|
|
59
|
+
# Authorize before resolving the agent: resolution reports whether a name
|
|
60
|
+
# matches an accessible agent, and a caller with no rights on the topic
|
|
61
|
+
# should not be able to ask.
|
|
62
|
+
authorize!(topic, name)
|
|
63
|
+
changed = requested(name, archived, primary_agent)
|
|
64
|
+
raise ArgumentError, "Nothing to update: pass name, archived, or primary_agent" if changed.empty?
|
|
65
|
+
|
|
66
|
+
agent = apply(topic, name, archived, primary_agent)
|
|
67
|
+
broadcast_all(topic.reload, changed, archived, agent)
|
|
68
|
+
|
|
69
|
+
Topics::Serializer.for_tool(topic).merge(changed: changed)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# A rename is gated at :admin to match TopicsController#update. Everything
|
|
75
|
+
# else here is a :write action, so the stricter gate is only applied when a
|
|
76
|
+
# rename is actually requested — otherwise archiving would silently need
|
|
77
|
+
# admin through this door and not through the UI.
|
|
78
|
+
def authorize!(topic, name)
|
|
79
|
+
name.present? ? TopicAuthorizer.authorize_admin!(topic) : TopicAuthorizer.authorize_write!(topic)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def requested(name, archived, primary_agent)
|
|
83
|
+
changed = []
|
|
84
|
+
changed << "name" if name.present?
|
|
85
|
+
changed << "archived" unless archived.nil?
|
|
86
|
+
changed << "primary_agent" unless primary_agent.nil?
|
|
87
|
+
changed
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Every requested field is checked before any of them is written, and the
|
|
91
|
+
# writes share one transaction. A call that renames, archives, and pins an
|
|
92
|
+
# agent the creative has not shared with must report failure over a topic
|
|
93
|
+
# that is still named what it was — not one that was quietly renamed and
|
|
94
|
+
# archived on the way to the error. Nothing broadcasts until the whole set
|
|
95
|
+
# has committed, for the same reason.
|
|
96
|
+
def apply(topic, name, archived, primary_agent)
|
|
97
|
+
agent = nil
|
|
98
|
+
Topic.transaction do
|
|
99
|
+
topic.lock!
|
|
100
|
+
authorize!(topic, name)
|
|
101
|
+
reject_reserved_mutation!(topic, name, archived)
|
|
102
|
+
agent = resolve_agent(topic, primary_agent)
|
|
103
|
+
topic.update!(name: name) if name.present?
|
|
104
|
+
set_archived(topic, archived) unless archived.nil?
|
|
105
|
+
topic.set_primary_agent!(agent) unless primary_agent.nil?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
agent
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def resolve_agent(topic, primary_agent)
|
|
112
|
+
change = Topics::AgentResolver.call(primary_agent, creative: topic.creative)
|
|
113
|
+
change ? validated_agent(topic, change) : nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def reject_reserved_mutation!(topic, name, archived)
|
|
117
|
+
return unless name.present? || !archived.nil?
|
|
118
|
+
return unless reserved_topic?(topic) || Topics::ReservedName.reserved?(topic.creative, name)
|
|
119
|
+
|
|
120
|
+
raise ArgumentError,
|
|
121
|
+
"Reserved topic names cannot be assigned or changed, and reserved topics cannot be archived."
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def reserved_topic?(topic)
|
|
125
|
+
Topics::ReservedName.reserved?(topic.creative, topic.name)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def set_archived(topic, archived)
|
|
129
|
+
archived ? topic.archive! : topic.unarchive!
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Mirrors TopicsController#set_primary_agent, including the session lock:
|
|
133
|
+
# on a Claude Channel session topic primary_agent_id is session identity,
|
|
134
|
+
# not a routing preference — Matcher needs it to route the agent at all and
|
|
135
|
+
# SessionProvisioner reuses the topic by it, so rewriting it would leave a
|
|
136
|
+
# live session unroutable and fork the next registration into a second topic.
|
|
137
|
+
def validated_agent(topic, agent_change)
|
|
138
|
+
raise ArgumentError, "This is a session topic; its agent assignment is locked." if topic.session_id.present?
|
|
139
|
+
|
|
140
|
+
agent = agent_change == Topics::AgentResolver::CLEAR ? nil : agent_change
|
|
141
|
+
reject_unassignable!(topic, agent)
|
|
142
|
+
agent
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def broadcast_all(topic, changed, archived, agent)
|
|
146
|
+
broadcast(topic, "updated", topic: Topics::Serializer.call(topic)) if changed.include?("name")
|
|
147
|
+
broadcast(topic, archived ? "archived" : "unarchived", topic: topic.slice(:id, :name, :archived_at)) if changed.include?("archived")
|
|
148
|
+
broadcast(topic, "updated", topic: Topics::Serializer.with_agent(topic, agent)) if changed.include?("primary_agent")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def reject_unassignable!(topic, agent)
|
|
152
|
+
return if agent.nil?
|
|
153
|
+
|
|
154
|
+
rejection = Topic.primary_agent_rejection(topic.creative, agent, topic: topic)
|
|
155
|
+
return unless rejection
|
|
156
|
+
|
|
157
|
+
raise ArgumentError, Topics::PinRejection.message(agent, rejection)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def broadcast(topic, action, payload)
|
|
161
|
+
TopicsChannel.broadcast_to(topic.creative, { action: action, **payload })
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -18,15 +18,21 @@ module Collavre
|
|
|
18
18
|
# does not apply.
|
|
19
19
|
# auto_select: false omits user_id from the topic-created broadcast so
|
|
20
20
|
# background/system branches do not hijack the owner's current selection.
|
|
21
|
-
|
|
21
|
+
# skip_approval_actions is reserved for system history copies that already
|
|
22
|
+
# selected an approval-free set but must tolerate a row becoming an approval
|
|
23
|
+
# prompt before the locked fetch. Interactive branches keep the default
|
|
24
|
+
# all-or-nothing rejection.
|
|
25
|
+
def call(comment_ids:, enforce_limit: true, auto_select: true, skip_approval_actions: false)
|
|
22
26
|
comment_ids = Array(comment_ids).map(&:presence).compact.map(&:to_i)
|
|
23
27
|
comment_ids = comment_ids.first(MAX_BRANCH_COMMENTS) if enforce_limit
|
|
24
28
|
raise BranchError, I18n.t("collavre.comments.branch.no_selection") if comment_ids.empty?
|
|
25
29
|
|
|
26
|
-
validate_permissions!
|
|
27
|
-
originals = fetch_comments(comment_ids)
|
|
28
|
-
|
|
29
30
|
Topic.transaction do
|
|
31
|
+
lock_source!
|
|
32
|
+
validate_permissions!
|
|
33
|
+
originals = fetch_comments(comment_ids)
|
|
34
|
+
originals = originals.reject(&:approval_action?) if skip_approval_actions
|
|
35
|
+
reject_approval_actions!(originals)
|
|
30
36
|
@new_topic = create_branch_topic
|
|
31
37
|
copy_comments(originals)
|
|
32
38
|
end
|
|
@@ -40,6 +46,16 @@ module Collavre
|
|
|
40
46
|
|
|
41
47
|
attr_reader :creative, :user, :source_topic, :name
|
|
42
48
|
|
|
49
|
+
def lock_source!
|
|
50
|
+
return unless source_topic
|
|
51
|
+
|
|
52
|
+
# The topic can move after a caller's preflight authorization. Pin its
|
|
53
|
+
# current creative under the same lock that protects the comment fetch so
|
|
54
|
+
# permissions and the branch destination cannot refer to stale state.
|
|
55
|
+
source_topic.lock!
|
|
56
|
+
@creative = source_topic.creative
|
|
57
|
+
end
|
|
58
|
+
|
|
43
59
|
def validate_permissions!
|
|
44
60
|
unless creative.has_permission?(user, :feedback)
|
|
45
61
|
raise BranchError, I18n.t("collavre.comments.branch.not_authorized")
|
|
@@ -48,15 +64,22 @@ module Collavre
|
|
|
48
64
|
|
|
49
65
|
def fetch_comments(comment_ids)
|
|
50
66
|
scope = source_topic ? source_topic.comments.visible_to(user) : creative.comments.visible_to(user)
|
|
51
|
-
comments = scope.where(id: comment_ids).order(:created_at).to_a
|
|
67
|
+
comments = scope.where(id: comment_ids).order(:created_at).lock.to_a
|
|
52
68
|
if comments.length != comment_ids.length
|
|
53
69
|
raise BranchError, I18n.t("collavre.comments.branch.comments_not_found")
|
|
54
70
|
end
|
|
55
71
|
comments
|
|
56
72
|
end
|
|
57
73
|
|
|
74
|
+
def reject_approval_actions!(comments)
|
|
75
|
+
return unless comments.any?(&:approval_action?)
|
|
76
|
+
|
|
77
|
+
raise BranchError, I18n.t("collavre.comments.branch.approval_action_not_branchable")
|
|
78
|
+
end
|
|
79
|
+
|
|
58
80
|
def create_branch_topic
|
|
59
81
|
topic_name = name.presence || default_branch_name
|
|
82
|
+
Topics::ReservedName.reject!(creative, topic_name, error_class: BranchError)
|
|
60
83
|
|
|
61
84
|
creative.topics.create!(
|
|
62
85
|
name: topic_name,
|
|
@@ -88,6 +111,12 @@ module Collavre
|
|
|
88
111
|
user_id: original.user_id,
|
|
89
112
|
content: original.content,
|
|
90
113
|
private: original.private,
|
|
114
|
+
# Carried with `private`, not separately from it: visible_to reads a
|
|
115
|
+
# private comment for its author *and* its approver, so copying the
|
|
116
|
+
# flag without the approver narrows who can see the copy. The user who
|
|
117
|
+
# selected the message can be the approver rather than the author, and
|
|
118
|
+
# would then get a branch reporting a message it cannot show them.
|
|
119
|
+
approver_id: original.approver_id,
|
|
91
120
|
review_type: original.review_type,
|
|
92
121
|
skip_default_user: true,
|
|
93
122
|
skip_dispatch: true
|
|
@@ -98,13 +127,13 @@ module Collavre
|
|
|
98
127
|
new_comment.quoted_comment_id = id_mapping[original.quoted_comment_id]
|
|
99
128
|
end
|
|
100
129
|
|
|
130
|
+
# Attach before validation so an image-only comment still satisfies the
|
|
131
|
+
# Comment content-or-images invariant. Reuse the blobs without copying
|
|
132
|
+
# file data, as the post-save loop did previously.
|
|
133
|
+
new_comment.images.attach(original.images.map(&:blob)) if original.images.attached?
|
|
134
|
+
|
|
101
135
|
new_comment.save!
|
|
102
136
|
id_mapping[original.id] = new_comment.id
|
|
103
|
-
|
|
104
|
-
# Share image blobs (no duplication of file data)
|
|
105
|
-
original.images.each do |image|
|
|
106
|
-
new_comment.images.attach(image.blob)
|
|
107
|
-
end
|
|
108
137
|
end
|
|
109
138
|
end
|
|
110
139
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Topics
|
|
5
|
+
# Resolves the `primary_agent` string an MCP caller passes into a User.
|
|
6
|
+
#
|
|
7
|
+
# The UI picks an agent from a palette and sends an id; an agent calling a
|
|
8
|
+
# tool has read a name out of a conversation and has no palette to click.
|
|
9
|
+
# Accepting id / email / display name is what makes topic_create and
|
|
10
|
+
# topic_update usable without a preceding lookup round-trip.
|
|
11
|
+
#
|
|
12
|
+
# Candidates are the owned-or-searchable set plus agents already shared on
|
|
13
|
+
# the target Creative. The latter matches User.mentionable_for and the UI's
|
|
14
|
+
# agent palette. Resolving over every agent row would let a caller confirm a
|
|
15
|
+
# private agent's existence by name, and would let them pin one they are not
|
|
16
|
+
# allowed to see.
|
|
17
|
+
module AgentResolver
|
|
18
|
+
# Sentinel distinguishing "clear the pin" from "no change". A blank string
|
|
19
|
+
# cannot carry that difference on its own: topic_update omitting the
|
|
20
|
+
# parameter and topic_update asking to unassign both arrive as falsy.
|
|
21
|
+
CLEAR = :clear
|
|
22
|
+
|
|
23
|
+
# What a caller writes to mean "unassign". Spelled out because an agent
|
|
24
|
+
# that guesses will guess one of these.
|
|
25
|
+
CLEAR_TOKENS = %w[none clear null nil unassign].freeze
|
|
26
|
+
|
|
27
|
+
class AmbiguousAgentError < ArgumentError; end
|
|
28
|
+
class UnknownAgentError < ArgumentError; end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Returns nil (no change), CLEAR (unassign), or a User.
|
|
33
|
+
def call(value, actor: Collavre::Current.user, creative: nil)
|
|
34
|
+
token = value.to_s.strip
|
|
35
|
+
return nil if value.nil?
|
|
36
|
+
return CLEAR if token.empty? || CLEAR_TOKENS.include?(token.downcase)
|
|
37
|
+
|
|
38
|
+
candidates = candidates_for(actor, creative)
|
|
39
|
+
find_by_id(candidates, token) ||
|
|
40
|
+
find_by_email(candidates, token) ||
|
|
41
|
+
find_by_name(candidates, token) ||
|
|
42
|
+
raise(UnknownAgentError, unknown_message(token))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def candidates_for(actor, creative)
|
|
46
|
+
accessible = User.accessible_ai_agents_for(actor)
|
|
47
|
+
return accessible unless creative
|
|
48
|
+
|
|
49
|
+
shared = User.mentionable_for(creative).ai_agents
|
|
50
|
+
base = User.ai_agents
|
|
51
|
+
base.where(id: accessible.reorder(nil).select(:id))
|
|
52
|
+
.or(base.where(id: shared.reorder(nil).select(:id)))
|
|
53
|
+
.distinct.order(:name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def find_by_id(candidates, token)
|
|
57
|
+
return nil unless token.match?(/\A\d+\z/)
|
|
58
|
+
|
|
59
|
+
candidates.find_by(id: token.to_i)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def find_by_email(candidates, token)
|
|
63
|
+
return nil unless token.include?("@")
|
|
64
|
+
|
|
65
|
+
candidates.find_by(email: token.downcase)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Names are not unique, so an exact-match tie is reported with the ids
|
|
69
|
+
# rather than silently resolved to the first row — picking one would
|
|
70
|
+
# quietly dedicate a topic to the wrong agent.
|
|
71
|
+
def find_by_name(candidates, token)
|
|
72
|
+
matches = candidates.where(name: token).to_a
|
|
73
|
+
return matches.first if matches.one?
|
|
74
|
+
return nil if matches.empty?
|
|
75
|
+
|
|
76
|
+
raise AmbiguousAgentError,
|
|
77
|
+
"Multiple agents named #{token.inspect}: ids #{matches.map(&:id).join(', ')}. Pass the id instead."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unknown_message(token)
|
|
81
|
+
"No accessible AI agent matches #{token.inspect}. " \
|
|
82
|
+
"Pass an agent id, email, or exact name; the agent must be searchable, created by you, " \
|
|
83
|
+
"or already shared on this creative."
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Topics
|
|
5
|
+
# A character cap and the unit it is measured in.
|
|
6
|
+
#
|
|
7
|
+
# The two travel together because neither is meaningful alone. A cap of
|
|
8
|
+
# 40,000 buys a different amount of conversation depending on whether the
|
|
9
|
+
# response is a markdown transcript or a JSON object, and budgeting in one
|
|
10
|
+
# unit while emitting the other is exactly how a cap gets exceeded by half
|
|
11
|
+
# again. Keeping them in one object means a caller cannot supply a cap and
|
|
12
|
+
# forget to say what it counts.
|
|
13
|
+
class CharBudget
|
|
14
|
+
FORMATS = %w[markdown json].freeze
|
|
15
|
+
DEFAULT_FORMAT = "markdown"
|
|
16
|
+
|
|
17
|
+
# Markdown's per-message overhead: the id, the ISO-8601 timestamp, the
|
|
18
|
+
# author separator and two newlines. Measured rather than guessed —
|
|
19
|
+
# content-only accounting hands this out for free, and a few hundred short
|
|
20
|
+
# messages is then an entire budget spent off the books.
|
|
21
|
+
ENVELOPE_CHARS = 36
|
|
22
|
+
|
|
23
|
+
# Markdown tags an AI author with a " (agent)" suffix on its byline. Eight
|
|
24
|
+
# characters is nothing on one message and a page's worth on a stretch of
|
|
25
|
+
# short agent turns, which is exactly the shape a busy topic has — and it
|
|
26
|
+
# is more than the margin the per-topic reserve leaves to absorb it.
|
|
27
|
+
AGENT_SUFFIX_CHARS = " (agent)".length
|
|
28
|
+
|
|
29
|
+
attr_reader :chars, :format
|
|
30
|
+
|
|
31
|
+
def self.normalize_format(value)
|
|
32
|
+
FORMATS.include?(value.to_s) ? value.to_s : DEFAULT_FORMAT
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(chars: nil, format: DEFAULT_FORMAT)
|
|
36
|
+
@chars = chars
|
|
37
|
+
@format = self.class.normalize_format(format)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unlimited? = @chars.nil?
|
|
41
|
+
|
|
42
|
+
def json? = @format == "json"
|
|
43
|
+
|
|
44
|
+
def with(chars:) = self.class.new(chars: chars, format: @format)
|
|
45
|
+
|
|
46
|
+
# What emitting this message costs, which is a property of the format
|
|
47
|
+
# rather than of the record.
|
|
48
|
+
#
|
|
49
|
+
# json is measured against the serialized form instead of estimated from
|
|
50
|
+
# it: the field names are a fixed ~120 characters the prose never sees,
|
|
51
|
+
# and the escaping ratio varies with the message, so no constant tracks
|
|
52
|
+
# both. The trailing character is the comma that joins it to the next one.
|
|
53
|
+
def cost(message)
|
|
54
|
+
return message.to_json.length + 1 if json?
|
|
55
|
+
|
|
56
|
+
ENVELOPE_CHARS + message[:content].to_s.length + message[:clip_notice].to_s.length +
|
|
57
|
+
message[:author].to_s.length +
|
|
58
|
+
(message[:agent] ? AGENT_SUFFIX_CHARS : 0)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether a message of this cost still fits alongside what has been spent.
|
|
62
|
+
# Takes the cost rather than the message so the caller can charge what it
|
|
63
|
+
# measured instead of serializing the same message twice.
|
|
64
|
+
#
|
|
65
|
+
# Deciding before keeping is the point: appending first and checking
|
|
66
|
+
# afterwards let a message wider than the entire cap through untouched.
|
|
67
|
+
def fits?(cost, spent: 0) = unlimited? || (spent + cost) <= @chars
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Topics
|
|
3
|
+
class LastTopicPreferencePayload
|
|
4
|
+
def initialize(creative:, user:)
|
|
5
|
+
@creative = creative
|
|
6
|
+
@user = user
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
preference = UserCreativePreference.find_by(user_id: @user&.id, creative_id: @creative.id)
|
|
11
|
+
{
|
|
12
|
+
last_topic_id: preference&.last_topic_id,
|
|
13
|
+
last_topic_revision: preference && [ preference.id, preference.last_topic_revision ]
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|