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,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
# First-run flow for the desktop CLI Proxy. Credential generation and the
|
|
5
|
+
# local proxy process live in Tauri; this controller only persists the
|
|
6
|
+
# resulting gateway after verifying a short-lived, local-only handoff token.
|
|
7
|
+
class DesktopSetupController < ApplicationController
|
|
8
|
+
allow_unauthenticated_access only: %i[show create_account]
|
|
9
|
+
|
|
10
|
+
STEPS = %w[account install adapters ready].freeze
|
|
11
|
+
DESKTOP_GATEWAY_NAME = "Collavre Desktop CLI Proxy"
|
|
12
|
+
PRESETS = {
|
|
13
|
+
"claude" => [ "Claude Code", "collavre-desktop-claude-code@ai.local", "paperclip/claude_local" ],
|
|
14
|
+
"codex" => [ "Codex", "collavre-desktop-codex@ai.local", "paperclip/codex_local" ]
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
@step = params[:step].to_s
|
|
19
|
+
@step = STEPS.first unless STEPS.include?(@step)
|
|
20
|
+
@user = Current.user
|
|
21
|
+
@account_required = !Collavre::User.where(system_admin: true).exists?
|
|
22
|
+
return if @account_required
|
|
23
|
+
|
|
24
|
+
# Recovery is entered without a step after native setup fails. An
|
|
25
|
+
# authenticated owner must resume at installation rather than see the
|
|
26
|
+
# sign-in-only account panel.
|
|
27
|
+
if @user&.system_admin?
|
|
28
|
+
@step = "install" if @step == "account"
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A desktop in open mode can have an ordinary user signed in when native
|
|
33
|
+
# proxy recovery opens this page. That user cannot mint a registration
|
|
34
|
+
# token, so keep them on the account step where they can sign out and use
|
|
35
|
+
# the owner's account instead of presenting a broken install action.
|
|
36
|
+
if @user
|
|
37
|
+
session[:return_to_after_authenticating] = collavre.desktop_setup_path(step: @step)
|
|
38
|
+
@step = "account"
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A failed native registration leaves the proxy installed but incomplete.
|
|
43
|
+
# Preserve the exact setup destination through login so the local owner
|
|
44
|
+
# can retry without deleting Keychain entries or app data.
|
|
45
|
+
@step = "install" if @step == "account"
|
|
46
|
+
session[:return_to_after_authenticating] = collavre.desktop_setup_path(step: @step)
|
|
47
|
+
redirect_to collavre.new_session_path
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create_account
|
|
51
|
+
return unless require_desktop_mode!
|
|
52
|
+
return unless require_loopback!
|
|
53
|
+
|
|
54
|
+
@user = Collavre::User.new(account_params)
|
|
55
|
+
Collavre::User.transaction do
|
|
56
|
+
unless @user.save
|
|
57
|
+
@step = "account"
|
|
58
|
+
@account_required = true
|
|
59
|
+
render :show, status: :unprocessable_entity
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The absence check is part of the UPDATE itself. Two overlapping local
|
|
64
|
+
# signups can both save, but only one can become the first administrator.
|
|
65
|
+
admin_exists = Collavre::User.where(system_admin: true).arel.exists
|
|
66
|
+
promoted = Collavre::User.where(id: @user.id)
|
|
67
|
+
.where(admin_exists.not)
|
|
68
|
+
.update_all(email_verified_at: Time.current, system_admin: true)
|
|
69
|
+
raise FirstAdministratorAlreadyCreated unless promoted == 1
|
|
70
|
+
end
|
|
71
|
+
start_new_session_for(@user)
|
|
72
|
+
redirect_to collavre.desktop_setup_path(step: :install)
|
|
73
|
+
rescue FirstAdministratorAlreadyCreated
|
|
74
|
+
redirect_to collavre.new_session_path, alert: t("collavre.desktop_setup.account.already_created")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def registration_token
|
|
78
|
+
return unless require_desktop_mode!
|
|
79
|
+
return unless require_loopback!
|
|
80
|
+
unless Current.user&.system_admin?
|
|
81
|
+
head :forbidden
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
render json: { token: registration_verifier.generate({ user_id: Current.user.id, expires_at: 5.minutes.from_now.to_i }) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
FirstAdministratorAlreadyCreated = Class.new(StandardError)
|
|
91
|
+
|
|
92
|
+
def account_params
|
|
93
|
+
params.require(:admin).permit(:name, :email, :password, :password_confirmation)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def registration_verifier
|
|
97
|
+
Rails.application.message_verifier("desktop-gateway-registration")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def require_loopback!
|
|
101
|
+
addr = IPAddr.new(request.remote_addr.to_s)
|
|
102
|
+
addr = addr.native if addr.respond_to?(:ipv4_mapped?) && addr.ipv4_mapped?
|
|
103
|
+
return true if addr.loopback?
|
|
104
|
+
|
|
105
|
+
head :forbidden
|
|
106
|
+
false
|
|
107
|
+
rescue IPAddr::InvalidAddressError
|
|
108
|
+
head :forbidden
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def require_desktop_mode!
|
|
113
|
+
return true if Rails.application.config.x.desktop_proxy_setup == true
|
|
114
|
+
|
|
115
|
+
head :not_found
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
# Authenticates the Rails child to the native shell before its webview is
|
|
5
|
+
# navigated to the local server. The secret is generated per launch by Tauri
|
|
6
|
+
# and exists only in this child process environment.
|
|
7
|
+
class DesktopSidecarHealthController < ActionController::API
|
|
8
|
+
def show
|
|
9
|
+
return unless require_loopback!
|
|
10
|
+
|
|
11
|
+
secret = ENV.fetch("COLLAVRE_DESKTOP_SIDECAR_SECRET", "")
|
|
12
|
+
return head :not_found if secret.blank?
|
|
13
|
+
|
|
14
|
+
challenge = params.require(:challenge)
|
|
15
|
+
digest = OpenSSL::HMAC.digest("SHA256", secret, challenge)
|
|
16
|
+
render plain: Base64.urlsafe_encode64(digest, padding: false), content_type: "text/plain"
|
|
17
|
+
rescue ActionController::ParameterMissing
|
|
18
|
+
head :bad_request
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def require_loopback!
|
|
24
|
+
addr = IPAddr.new(request.remote_addr.to_s)
|
|
25
|
+
addr = addr.native if addr.respond_to?(:ipv4_mapped?) && addr.ipv4_mapped?
|
|
26
|
+
return true if addr.loopback?
|
|
27
|
+
|
|
28
|
+
head :forbidden
|
|
29
|
+
false
|
|
30
|
+
rescue IPAddr::InvalidAddressError
|
|
31
|
+
head :forbidden
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
# Public, server-rendered guide pages for the empty-chat feature cards.
|
|
5
|
+
#
|
|
6
|
+
# These double as landing content, so they are readable signed out and share
|
|
7
|
+
# the landing layout/stylesheet. All copy lives in config/locales/features.*.yml
|
|
8
|
+
# under collavre.features.pages.<key>; the registry only supplies which keys
|
|
9
|
+
# exist and in what order.
|
|
10
|
+
class FeaturesController < ApplicationController
|
|
11
|
+
allow_unauthenticated_access
|
|
12
|
+
layout "collavre/landing"
|
|
13
|
+
|
|
14
|
+
def index
|
|
15
|
+
@cards = Collavre::FeatureCardRegistry.with_builtin_guide
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
@card = Collavre::FeatureCardRegistry.find(params[:key])
|
|
20
|
+
# An unknown key, or a card documented by a vendor engine elsewhere, has no
|
|
21
|
+
# page here — both are a 404 rather than a blank shell of headings.
|
|
22
|
+
raise ActiveRecord::RecordNotFound unless @card&.builtin_guide?
|
|
23
|
+
|
|
24
|
+
@page = self.class.page_content(@card.key)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reads a page's copy out of i18n, normalizing the parts a translator can
|
|
28
|
+
# legitimately omit. Missing translations come back from I18n as a String
|
|
29
|
+
# rather than the Array/Hash shape the view walks, so coerce before render
|
|
30
|
+
# instead of letting the view guard every access.
|
|
31
|
+
def self.page_content(key)
|
|
32
|
+
scope = "collavre.features.pages.#{key}"
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
title: I18n.t("#{scope}.title", default: ""),
|
|
36
|
+
tagline: I18n.t("#{scope}.tagline", default: ""),
|
|
37
|
+
meta_description: I18n.t("#{scope}.meta_description", default: ""),
|
|
38
|
+
sections: array_of_hashes(I18n.t("#{scope}.sections", default: [])),
|
|
39
|
+
tips: array_of_strings(I18n.t("#{scope}.tips", default: []))
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.array_of_hashes(value)
|
|
44
|
+
return [] unless value.is_a?(Array)
|
|
45
|
+
|
|
46
|
+
value.select { |entry| entry.is_a?(Hash) && entry[:body].present? }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.array_of_strings(value)
|
|
50
|
+
return [] unless value.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
value.select { |entry| entry.is_a?(String) && entry.present? }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_class_method :array_of_hashes, :array_of_strings
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class InvitesController < ApplicationController
|
|
3
|
+
LINK_PREVIEW_USER_AGENT_PATTERN = /
|
|
4
|
+
bot\b|crawler\b|spider\b|preview\b|scrap(?:e|er)?\b|
|
|
5
|
+
facebookexternalhit|whatsapp|iframely|embedly
|
|
6
|
+
/ix
|
|
7
|
+
|
|
3
8
|
allow_unauthenticated_access only: :show
|
|
4
9
|
before_action :set_invitation, only: :show
|
|
5
10
|
|
|
@@ -17,11 +22,17 @@ module Collavre
|
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def show
|
|
20
|
-
@invitation.
|
|
25
|
+
return if @invitation.clicked_at || link_preview_crawler?
|
|
26
|
+
|
|
27
|
+
@invitation.update(clicked_at: Time.current)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
private
|
|
24
31
|
|
|
32
|
+
def link_preview_crawler?
|
|
33
|
+
request.user_agent.to_s.match?(LINK_PREVIEW_USER_AGENT_PATTERN)
|
|
34
|
+
end
|
|
35
|
+
|
|
25
36
|
def set_invitation
|
|
26
37
|
@invitation = Invitation.find_by_token_for(:invite, params[:token])
|
|
27
38
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
class NoticesController < ApplicationController
|
|
3
|
+
def dismiss
|
|
4
|
+
key = params[:key].to_s
|
|
5
|
+
|
|
6
|
+
Current.user.with_lock do
|
|
7
|
+
dismissed = Current.user.dismissed_notices || []
|
|
8
|
+
Current.user.update!(dismissed_notices: (dismissed + [ key ]).uniq)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
render json: { success: true, dismissed_notices: Current.user.dismissed_notices }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def restore_all
|
|
15
|
+
Current.user.update!(dismissed_notices: [])
|
|
16
|
+
|
|
17
|
+
render json: { success: true, dismissed_notices: Current.user.dismissed_notices }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module Collavre
|
|
4
4
|
class TasksController < ApplicationController
|
|
5
|
+
def active_statuses
|
|
6
|
+
task_ids = params[:task_ids].to_s.split(",").map(&:to_i).reject(&:zero?)
|
|
7
|
+
return render json: { tasks: [] } if task_ids.empty?
|
|
8
|
+
|
|
9
|
+
tasks = Task.where(id: task_ids).includes(:creative)
|
|
10
|
+
results = tasks.filter_map do |task|
|
|
11
|
+
creative = task.creative || Creative.find_by(id: task.trigger_event_payload&.dig("creative", "id"))
|
|
12
|
+
next unless creative&.has_permission?(Current.user, :read)
|
|
13
|
+
|
|
14
|
+
{ id: task.id, active: task.active? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
render json: { tasks: results }
|
|
18
|
+
end
|
|
19
|
+
|
|
5
20
|
def cancel
|
|
6
21
|
task = Task.find(params[:id])
|
|
7
22
|
creative = task.creative || Creative.find_by(id: task.trigger_event_payload&.dig("creative", "id"))
|
|
@@ -10,10 +25,6 @@ module Collavre
|
|
|
10
25
|
return head :forbidden
|
|
11
26
|
end
|
|
12
27
|
|
|
13
|
-
unless %w[running pending queued pending_approval delegated].include?(task.status)
|
|
14
|
-
return head :unprocessable_entity
|
|
15
|
-
end
|
|
16
|
-
|
|
17
28
|
# These statuses count against the topic slot (occupying_topic_slot) yet no
|
|
18
29
|
# live worker will run AiAgentJob's ensure-block drain for them:
|
|
19
30
|
# - delegated / pending_approval already returned from the job holding the
|
|
@@ -25,8 +36,31 @@ module Collavre
|
|
|
25
36
|
# cancelling the blocker leaves agent capacity and the next waiter stuck
|
|
26
37
|
# until stuck recovery. release!/dequeue are idempotent (dequeue is bounded
|
|
27
38
|
# by topic_at_capacity?), so a racing live worker that also drains is harmless.
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
previous_status = task.cancel_if_active!
|
|
40
|
+
return head :unprocessable_entity unless previous_status
|
|
41
|
+
|
|
42
|
+
held_slot_without_worker = Task::HELD_SLOT_WITHOUT_WORKER.include?(previous_status)
|
|
43
|
+
|
|
44
|
+
# The third door a waiter leaves the queue through without ever being
|
|
45
|
+
# promoted — "queued" is in the whitelist above, so the user's own stop
|
|
46
|
+
# button is one of them. Its "task" notice names this task, and a notice
|
|
47
|
+
# naming a task that is no longer queued cancels nothing when dismissed:
|
|
48
|
+
# a stop button for work that can no longer be stopped. Nothing else
|
|
49
|
+
# collects it either — the task is never promoted, so
|
|
50
|
+
# cleanup_waiter_notice! never runs for it, and the drained sweep needs
|
|
51
|
+
# the topic queue to empty, which any sibling waiter prevents.
|
|
52
|
+
Comment.remove_waiter_notices!(
|
|
53
|
+
creative_id: task.creative_id, topic_id: task.topic_id, task_ids: task.id
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# With coalescing on this waiter had no notice of its own — the topic's
|
|
57
|
+
# shared one spoke for it, and the call above deliberately does not touch
|
|
58
|
+
# that kind. Same stranding, one row over.
|
|
59
|
+
if task.creative_id
|
|
60
|
+
Comment.remove_stranded_waiting_notices!(
|
|
61
|
+
creative_id: task.creative_id, topic_id: task.topic_id
|
|
62
|
+
)
|
|
63
|
+
end
|
|
30
64
|
|
|
31
65
|
if held_slot_without_worker && task.agent
|
|
32
66
|
Collavre::Orchestration::ResourceTracker.for(task.agent).release!(task.id)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class TopicsController < ApplicationController
|
|
3
|
+
rescue_from Topics::TopicMove::SourceChangedError, with: :render_source_changed
|
|
4
|
+
rescue_from Topics::TopicMove::MoveBlockedError, with: :render_active_tasks
|
|
5
|
+
|
|
3
6
|
include Collavre::CreativePermissionGuard
|
|
4
7
|
|
|
5
8
|
before_action :set_creative
|
|
@@ -10,7 +13,11 @@ module Collavre
|
|
|
10
13
|
def index
|
|
11
14
|
is_owner = @creative.user == Current.user
|
|
12
15
|
can_manage = @creative.has_permission?(Current.user, :admin) || is_owner
|
|
13
|
-
|
|
16
|
+
# Mirrors require_creative_write! — the gate on #create and
|
|
17
|
+
# #set_primary_agent. Reported as two capabilities because the client gates
|
|
18
|
+
# two separate controls on it, and only #update/#destroy/#move/#reorder
|
|
19
|
+
# need :admin.
|
|
20
|
+
can_write = can_manage || @creative.has_permission?(Current.user, :write)
|
|
14
21
|
|
|
15
22
|
# Eagerly ensure Main (and System for inboxes) exist BEFORE loading
|
|
16
23
|
# active_topics. Otherwise the first inbox visit sees no System topic in
|
|
@@ -21,23 +28,20 @@ module Collavre
|
|
|
21
28
|
system_topic = @creative.inbox? ? @creative.system_topic(fallback_user: Current.user) : nil
|
|
22
29
|
|
|
23
30
|
active_topics = @creative.topics.active.includes(primary_agent: { avatar_attachment: :blob }).order(:created_at).to_a
|
|
24
|
-
archived_topics = @creative.topics.archived.order(:created_at)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
UserCreativePreference
|
|
28
|
-
.where(user_id: Current.user.id, creative_id: @creative.id)
|
|
29
|
-
.pick(:last_topic_id)
|
|
30
|
-
end
|
|
31
|
+
archived_topics = @creative.topics.archived.includes(primary_agent: { avatar_attachment: :blob }).order(:created_at).to_a
|
|
32
|
+
unread_counts_by_topic = Creatives::CommentBadgeIndex.new(user: Current.user)
|
|
33
|
+
.unread_counts_by_topic(@creative)
|
|
31
34
|
|
|
32
35
|
system_topic_id = system_topic&.id
|
|
33
36
|
main_topic_id = main_topic.id
|
|
34
37
|
|
|
35
38
|
render json: {
|
|
36
|
-
topics: active_topics.map { |t| topic_json(t) },
|
|
37
|
-
archived_topics: archived_topics,
|
|
39
|
+
topics: active_topics.map { |t| topic_json(t, unread_count: unread_counts_by_topic.fetch(t.id, 0)) },
|
|
40
|
+
archived_topics: archived_topics.map { |t| topic_json(t, unread_count: unread_counts_by_topic.fetch(t.id, 0)) },
|
|
38
41
|
can_manage: can_manage,
|
|
39
|
-
can_create_topic:
|
|
40
|
-
|
|
42
|
+
can_create_topic: can_write,
|
|
43
|
+
can_set_primary_agent: can_write,
|
|
44
|
+
**Topics::LastTopicPreferencePayload.new(creative: @creative, user: Current.user).call,
|
|
41
45
|
is_inbox: @creative.inbox?,
|
|
42
46
|
system_topic_id: system_topic_id,
|
|
43
47
|
main_topic_id: main_topic_id,
|
|
@@ -46,15 +50,27 @@ module Collavre
|
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def create
|
|
53
|
+
requested_agent = params[:agent_id].present? ? User.find_by(id: params[:agent_id]) : nil
|
|
54
|
+
|
|
55
|
+
# No topic yet, so a Claude Channel agent is rejected on an inbox creative:
|
|
56
|
+
# a topic created here never carries a session_id, which is the only place
|
|
57
|
+
# Matcher#eligible_in_inbox? will route one.
|
|
58
|
+
rejection = requested_agent&.ai_user? &&
|
|
59
|
+
Topic.primary_agent_rejection(@creative, requested_agent)
|
|
60
|
+
if rejection
|
|
61
|
+
render json: { errors: [ unassignable_agent_message(requested_agent, rejection) ] },
|
|
62
|
+
status: :unprocessable_entity and return
|
|
63
|
+
end
|
|
64
|
+
|
|
49
65
|
topic = @creative.topics.build(topic_params)
|
|
50
66
|
topic.user = Current.user
|
|
51
67
|
|
|
52
68
|
Topic.transaction do
|
|
53
69
|
if topic.save
|
|
54
70
|
agent = nil
|
|
55
|
-
if
|
|
56
|
-
agent =
|
|
57
|
-
topic.set_primary_agent!(agent)
|
|
71
|
+
if requested_agent&.ai_user?
|
|
72
|
+
agent = requested_agent
|
|
73
|
+
topic.set_primary_agent!(agent)
|
|
58
74
|
end
|
|
59
75
|
|
|
60
76
|
# Move comments to the new topic if comment_ids provided
|
|
@@ -89,7 +105,7 @@ module Collavre
|
|
|
89
105
|
def update
|
|
90
106
|
topic = @creative.topics.find(params[:id])
|
|
91
107
|
|
|
92
|
-
if topic.update(topic_params)
|
|
108
|
+
if mutate_locked_topic(topic) { |current_topic| current_topic.update(topic_params) }
|
|
93
109
|
broadcast_topic_event("updated", topic: topic_json(topic))
|
|
94
110
|
render json: topic_json(topic)
|
|
95
111
|
else
|
|
@@ -104,9 +120,9 @@ module Collavre
|
|
|
104
120
|
render json: { error: I18n.t("collavre.topics.cannot_delete_main") }, status: :unprocessable_entity and return
|
|
105
121
|
end
|
|
106
122
|
|
|
107
|
-
topic_id =
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
topic_id, topic_name = Topics::TopicDestroy.new(
|
|
124
|
+
topic: topic, source_creative: @creative
|
|
125
|
+
).call
|
|
110
126
|
|
|
111
127
|
# Best-effort orphaned-cron notice. Must never break the core deletion:
|
|
112
128
|
# if it raises, swallow + log so broadcast/head still run.
|
|
@@ -140,31 +156,37 @@ module Collavre
|
|
|
140
156
|
render json: { error: I18n.t("collavre.topics.move.duplicate_name", name: topic.name) }, status: :unprocessable_entity and return
|
|
141
157
|
end
|
|
142
158
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
# A Claude Channel session topic is looked up as
|
|
160
|
+
# inbox.topics.find_by(primary_agent_id:, session_id:) — re-parenting it
|
|
161
|
+
# takes it out of that scope, so the next registration would fork a second
|
|
162
|
+
# topic and Matcher#eligible_in_inbox? would stop routing the session
|
|
163
|
+
# agent. Same identity that #set_primary_agent refuses to rewrite.
|
|
164
|
+
if topic.session_id.present?
|
|
165
|
+
render json: { error: I18n.t("collavre.topics.move.session_topic_locked") }, status: :unprocessable_entity and return
|
|
146
166
|
end
|
|
147
167
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
broadcast_topic_event("deleted", topic_id: topic.id)
|
|
154
|
-
broadcast_topic_event("created", creative: target_creative, topic: topic.slice(:id, :name))
|
|
168
|
+
effects = Topics::TopicMoveEffects.new(topic, source_creative, target_creative)
|
|
169
|
+
released_agent, released_reason = Topics::TopicMove.new(
|
|
170
|
+
topic: topic, target_creative: target_creative
|
|
171
|
+
).call(after_commit: effects.method(:call))
|
|
155
172
|
|
|
156
173
|
render json: {
|
|
157
174
|
success: true,
|
|
158
175
|
topic: topic.slice(:id, :name),
|
|
159
176
|
target_creative_id: target_creative.id,
|
|
160
177
|
target_creative_name: target_creative.creative_snippet,
|
|
161
|
-
missing_members: missing_members_for_move(source_creative, target_creative)
|
|
178
|
+
missing_members: missing_members_for_move(source_creative, target_creative),
|
|
179
|
+
released_primary_agent: released_agent && {
|
|
180
|
+
id: released_agent.id,
|
|
181
|
+
name: released_agent.display_name,
|
|
182
|
+
message: released_primary_agent_message(released_agent, released_reason, target_creative)
|
|
183
|
+
}
|
|
162
184
|
}
|
|
163
185
|
end
|
|
164
186
|
|
|
165
187
|
def archive
|
|
166
188
|
topic = @creative.topics.find(params[:id])
|
|
167
|
-
topic
|
|
189
|
+
mutate_locked_topic(topic, &:archive!)
|
|
168
190
|
|
|
169
191
|
broadcast_topic_event("archived", topic: topic.slice(:id, :name))
|
|
170
192
|
render json: { success: true }
|
|
@@ -172,7 +194,7 @@ module Collavre
|
|
|
172
194
|
|
|
173
195
|
def unarchive
|
|
174
196
|
topic = @creative.topics.find(params[:id])
|
|
175
|
-
topic
|
|
197
|
+
mutate_locked_topic(topic, &:unarchive!)
|
|
176
198
|
|
|
177
199
|
broadcast_topic_event("unarchived", topic: topic.slice(:id, :name, :archived_at))
|
|
178
200
|
render json: { success: true }
|
|
@@ -197,13 +219,44 @@ module Collavre
|
|
|
197
219
|
|
|
198
220
|
def set_primary_agent
|
|
199
221
|
topic = @creative.topics.find(params[:id])
|
|
222
|
+
|
|
223
|
+
# On a Claude Channel session topic, primary_agent_id is not a routing pin
|
|
224
|
+
# but part of the session identity: Matcher#eligible_in_inbox? needs it to
|
|
225
|
+
# route the session agent at all, and SessionProvisioner#find_or_create_topic
|
|
226
|
+
# reuses the topic by (primary_agent_id, session_id). Clearing or reassigning
|
|
227
|
+
# it would leave a live session unroutable and split the next registration
|
|
228
|
+
# into a second topic. Releasing a session is session teardown, not an
|
|
229
|
+
# avatar click.
|
|
230
|
+
if topic.session_id.present?
|
|
231
|
+
render json: { error: I18n.t("collavre.topics.session_topic_locked") },
|
|
232
|
+
status: :unprocessable_entity and return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# A blank agent_id clears the assignment. The primary agent is an exclusive
|
|
236
|
+
# assignment (it silences every other agent's ambient routing), so the user
|
|
237
|
+
# needs a way back to an unassigned topic — otherwise a single avatar click
|
|
238
|
+
# would permanently dedicate the topic to one agent.
|
|
239
|
+
if params[:agent_id].blank?
|
|
240
|
+
mutate_locked_topic(topic) { |current_topic| current_topic.set_primary_agent!(nil) }
|
|
241
|
+
|
|
242
|
+
broadcast_topic_event("updated", topic: topic_json_with_agent(topic, nil))
|
|
243
|
+
|
|
244
|
+
return render json: { success: true, topic: topic_json_with_agent(topic, nil) }
|
|
245
|
+
end
|
|
246
|
+
|
|
200
247
|
agent = User.find_by(id: params[:agent_id])
|
|
201
248
|
|
|
202
249
|
unless agent&.ai_user?
|
|
203
250
|
render json: { error: I18n.t("collavre.topics.not_ai_agent") }, status: :unprocessable_entity and return
|
|
204
251
|
end
|
|
205
252
|
|
|
206
|
-
|
|
253
|
+
rejection = Topic.primary_agent_rejection(@creative, agent, topic: topic)
|
|
254
|
+
if rejection
|
|
255
|
+
render json: { error: unassignable_agent_message(agent, rejection) },
|
|
256
|
+
status: :unprocessable_entity and return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
mutate_locked_topic(topic) { |current_topic| current_topic.set_primary_agent!(agent) }
|
|
207
260
|
|
|
208
261
|
broadcast_topic_event("updated", topic: topic_json_with_agent(topic, agent))
|
|
209
262
|
|
|
@@ -212,10 +265,43 @@ module Collavre
|
|
|
212
265
|
|
|
213
266
|
private
|
|
214
267
|
|
|
268
|
+
def render_source_changed(error)
|
|
269
|
+
render json: { error: error.message }, status: :forbidden
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def render_active_tasks(error)
|
|
273
|
+
render json: { error: error.message }, status: :unprocessable_entity
|
|
274
|
+
end
|
|
275
|
+
|
|
215
276
|
def set_creative
|
|
216
277
|
@creative = Creative.find(params[:creative_id]).effective_origin
|
|
217
278
|
end
|
|
218
279
|
|
|
280
|
+
def mutate_locked_topic(topic, &) = Topics::TopicMutation.new(topic: topic, source_creative: @creative).call(&)
|
|
281
|
+
|
|
282
|
+
# A move can now release the pin for either reason, and the advice differs:
|
|
283
|
+
# sharing the target creative fixes a missing-permission release, but a
|
|
284
|
+
# Claude Channel session agent stays confined to its own session topic no
|
|
285
|
+
# matter what is shared — telling the user to share and re-assign would send
|
|
286
|
+
# them after a fix that cannot work.
|
|
287
|
+
def released_primary_agent_message(agent, rejection, target_creative)
|
|
288
|
+
key = if rejection == :session_agent_outside_session_topic
|
|
289
|
+
"collavre.topics.move.primary_agent_released_session_agent"
|
|
290
|
+
else
|
|
291
|
+
"collavre.topics.move.primary_agent_released"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
I18n.t(key, agent: agent.display_name, creative: target_creative.creative_snippet)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def unassignable_agent_message(agent, rejection)
|
|
298
|
+
if rejection == :session_agent_outside_session_topic
|
|
299
|
+
I18n.t("collavre.topics.session_agent_not_assignable", name: agent.display_name)
|
|
300
|
+
else
|
|
301
|
+
I18n.t("collavre.topics.agent_no_creative_access", name: agent.display_name)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
219
305
|
# When a topic moves to a different creative, members who had access on the
|
|
220
306
|
# source creative may lose visibility on the target. Returns the human
|
|
221
307
|
# members who are effectively present on the source (owner + feedback-or-
|
|
@@ -257,36 +343,27 @@ module Collavre
|
|
|
257
343
|
end
|
|
258
344
|
end
|
|
259
345
|
|
|
346
|
+
|
|
260
347
|
def topic_params
|
|
261
348
|
params.require(:topic).permit(:name)
|
|
262
349
|
end
|
|
263
350
|
|
|
264
351
|
def generate_next_topic_name
|
|
265
|
-
|
|
266
|
-
existing_numbers = @creative.topics.active
|
|
267
|
-
.where("name LIKE ?", "#{Topic.sanitize_sql_like(prefix)}%")
|
|
268
|
-
.pluck(:name)
|
|
269
|
-
.filter_map { |n|
|
|
270
|
-
suffix = n.delete_prefix(prefix)
|
|
271
|
-
suffix.match?(/\A\d+\z/) ? suffix.to_i : nil
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
next_number = (existing_numbers.max || 0) + 1
|
|
275
|
-
"#{prefix}#{next_number}"
|
|
352
|
+
Topics::NextName.for(@creative)
|
|
276
353
|
end
|
|
277
354
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
data
|
|
355
|
+
# Delegated to Topics::Serializer so the topic MCP tools, which broadcast the
|
|
356
|
+
# same events from outside a request, cannot describe a topic differently
|
|
357
|
+
# from this controller.
|
|
358
|
+
def topic_json(topic, unread_count: nil)
|
|
359
|
+
Topics::Serializer.call(topic, unread_count: unread_count)
|
|
284
360
|
end
|
|
285
361
|
|
|
362
|
+
# Always carries a :primary_agent key, explicitly nil when cleared. The client
|
|
363
|
+
# merges this payload into its cached topic, so an omitted key would leave a
|
|
364
|
+
# stale avatar on screen instead of removing it.
|
|
286
365
|
def topic_json_with_agent(topic, agent)
|
|
287
|
-
|
|
288
|
-
data[:primary_agent] = agent_json(agent)
|
|
289
|
-
data
|
|
366
|
+
Topics::Serializer.with_agent(topic, agent)
|
|
290
367
|
end
|
|
291
368
|
|
|
292
369
|
def agent_json(agent)
|