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
|
@@ -24,19 +24,54 @@ class CommentsPresenceChannel < ApplicationCable::Channel
|
|
|
24
24
|
)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
# Replayed to late subscribers. "pending_approval" must be here: its job already
|
|
28
|
+
# returned, so no heartbeat or later broadcast will ever re-announce it, and a
|
|
29
|
+
# reloading viewer would lose the Stop button on the turn that is waiting on them.
|
|
30
|
+
AGENT_REPLAY_STATUSES = %w[running pending pending_approval].freeze
|
|
31
|
+
|
|
32
|
+
# Narrowed by creative in SQL, not in Ruby: a pending_approval turn sits until
|
|
33
|
+
# somebody answers it, so the replayable set is unbounded and deployment-wide,
|
|
34
|
+
# and every subscribe would otherwise load all of it just to keep one creative's
|
|
35
|
+
# rows. tasks.creative_id is written from the same context["creative"]["id"] the
|
|
36
|
+
# payload carries (AiAgentJob#admit_or_defer!, park_waiter), and is indexed.
|
|
37
|
+
# A row can only lose it to the FK's on_delete:
|
|
38
|
+
# :nullify, and a deleted creative already returns [] above.
|
|
39
|
+
#
|
|
40
|
+
# Ordered by id because the client appends these into a per-agent array and stops
|
|
41
|
+
# the last one. Not created_at: turns are stamped by whichever process dispatched
|
|
42
|
+
# them, so a web/worker clock skew can invert two of them (see PR #1431).
|
|
43
|
+
def self.replayable_tasks(creative_id)
|
|
44
|
+
Task.where(status: AGENT_REPLAY_STATUSES, creative_id: creative_id).order(:id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Scoped to the creative the popup is open on, not to its origin: the client keeps
|
|
48
|
+
# that id as this.creativeId and drops any payload naming a different one, and a
|
|
49
|
+
# link is a placement — replaying every task under the origin would put another
|
|
50
|
+
# user's shell id on the wire, which PermissionFilter#readable_ids exists to hide.
|
|
51
|
+
# Topics hang off the origin, so that is what resolves a task's topic_id.
|
|
52
|
+
def self.running_agent_payloads(creative_id, topic_id: nil)
|
|
53
|
+
creative = Creative.find_by(id: creative_id)&.effective_origin
|
|
54
|
+
return [] unless creative
|
|
55
|
+
|
|
56
|
+
replayable_tasks(creative_id).filter_map do |task|
|
|
57
|
+
# Still checked: the column is what the query trusts, the payload is what the
|
|
58
|
+
# client is handed back as source_creative_id, and a row where the two
|
|
59
|
+
# disagree must not name a creative it was not dispatched on.
|
|
31
60
|
task_creative_id = task.trigger_event_payload&.dig("creative", "id")
|
|
32
61
|
next unless task_creative_id == creative_id
|
|
33
62
|
|
|
34
|
-
|
|
63
|
+
task_topic_id = AiAgent::AgentLifecycleManager.topic_id_for(task: task, creative: creative)
|
|
64
|
+
next if topic_id && task_topic_id.to_s != topic_id.to_s
|
|
65
|
+
|
|
66
|
+
agent_status_payload(
|
|
35
67
|
creative_id,
|
|
36
|
-
status
|
|
68
|
+
# Replayed with its real status, not a blanket "thinking": the client keys
|
|
69
|
+
# its own handling off this string, and a paused turn is not a working one.
|
|
70
|
+
status: task.status == "pending_approval" ? "pending_approval" : "thinking",
|
|
37
71
|
agent_id: task.agent_id,
|
|
38
72
|
agent_name: task.agent.display_name,
|
|
39
73
|
task_id: task.id,
|
|
74
|
+
topic_id: task_topic_id,
|
|
40
75
|
source_creative_id: task_creative_id
|
|
41
76
|
)
|
|
42
77
|
end
|
|
@@ -45,62 +80,187 @@ class CommentsPresenceChannel < ApplicationCable::Channel
|
|
|
45
80
|
# Broadcast agent status (thinking/streaming/idle) to presence channel.
|
|
46
81
|
# This allows the frontend typing indicator to show AI agent activity.
|
|
47
82
|
# source_creative_id: the actual creative where agent is working (for filtering on frontend)
|
|
48
|
-
def self.broadcast_agent_status(creative_id, status:, agent_id:, agent_name:, task_id: nil, content: nil, source_creative_id: nil)
|
|
83
|
+
def self.broadcast_agent_status(creative_id, status:, agent_id:, agent_name:, topic_id:, task_id: nil, content: nil, source_creative_id: nil)
|
|
84
|
+
ActionCable.server.broadcast(
|
|
85
|
+
"comments_presence:#{creative_id}",
|
|
86
|
+
agent_status_payload(
|
|
87
|
+
creative_id,
|
|
88
|
+
status: status, agent_id: agent_id, agent_name: agent_name, topic_id: topic_id,
|
|
89
|
+
task_id: task_id, content: content, source_creative_id: source_creative_id
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.agent_status_payload(creative_id, status:, agent_id:, agent_name:, topic_id:, task_id: nil, content: nil, source_creative_id: nil)
|
|
49
95
|
payload = {
|
|
50
96
|
agent_status: {
|
|
51
97
|
id: agent_id,
|
|
52
98
|
name: agent_name,
|
|
53
99
|
status: status,
|
|
54
100
|
task_id: task_id,
|
|
101
|
+
topic_id: topic_id,
|
|
55
102
|
creative_id: source_creative_id || creative_id
|
|
56
103
|
}
|
|
57
104
|
}
|
|
58
105
|
payload[:agent_status][:content] = content if content.present?
|
|
59
|
-
|
|
106
|
+
payload
|
|
60
107
|
end
|
|
61
108
|
|
|
62
109
|
def subscribed
|
|
63
110
|
Rails.logger.info "User #{current_user&.email} subscribed to comments presence for creative #{params[:creative_id]}"
|
|
64
|
-
return unless params[:creative_id].present? && current_user
|
|
111
|
+
return reject unless params[:creative_id].present? && current_user
|
|
112
|
+
|
|
113
|
+
requested = Creative.find_by(id: params[:creative_id].to_i)
|
|
114
|
+
creative = requested&.effective_origin
|
|
115
|
+
# The only gate on this channel — nothing below is re-checked per message, and
|
|
116
|
+
# active_statuses already filters the same task data by :read.
|
|
117
|
+
return reject unless creative&.has_permission?(current_user, :read)
|
|
65
118
|
|
|
66
|
-
@creative_id =
|
|
67
|
-
|
|
119
|
+
@creative_id = creative.id
|
|
120
|
+
# Kept for the whole subscription, not just for the replay below: comments live on
|
|
121
|
+
# the origin, so everything else on this channel is keyed by @creative_id, while
|
|
122
|
+
# a task is dispatched on — and replayed for — the creative the popup is open on.
|
|
123
|
+
# #running_agents replays too, and reading @creative_id there would search the
|
|
124
|
+
# origin for rows that only ever carried the shell's id.
|
|
125
|
+
@requested_creative_id = requested.id
|
|
126
|
+
@presence_subscription_id = SecureRandom.uuid
|
|
68
127
|
stream_from stream_name
|
|
69
|
-
CommentPresenceStore.add(@creative_id, current_user.id)
|
|
128
|
+
CommentPresenceStore.add(@creative_id, current_user.id, subscription_id: @presence_subscription_id)
|
|
70
129
|
Comment.broadcast_badge(creative, current_user)
|
|
71
130
|
broadcast_presence
|
|
72
|
-
|
|
131
|
+
# Transmitted, not broadcast: stream_from attaches asynchronously, so a broadcast
|
|
132
|
+
# published in the same breath can be dropped. Presence and badges survive that
|
|
133
|
+
# (later events re-send them); this replay is one-shot.
|
|
134
|
+
replay_running_agents
|
|
73
135
|
end
|
|
74
136
|
|
|
75
137
|
def unsubscribed
|
|
76
138
|
if @creative_id && current_user
|
|
77
|
-
CommentPresenceStore.remove(@creative_id, current_user.id)
|
|
139
|
+
CommentPresenceStore.remove(@creative_id, current_user.id, subscription_id: @presence_subscription_id)
|
|
78
140
|
creative = Creative.find(@creative_id)
|
|
79
|
-
pointer = CommentReadPointer.find_or_initialize_by(user: current_user, creative: creative)
|
|
80
|
-
pointer.last_read_comment_id = creative.comments.maximum(:id)
|
|
81
|
-
pointer.save!
|
|
82
141
|
Comment.broadcast_badge(creative, current_user)
|
|
83
142
|
broadcast_presence
|
|
84
143
|
end
|
|
85
144
|
end
|
|
86
145
|
|
|
87
|
-
|
|
146
|
+
# Presence is creative-wide for participant avatars, but unread suppression is
|
|
147
|
+
# topic-specific. A client reports the topic it is actively rendering so a
|
|
148
|
+
# message in another topic keeps its badge while the chat popup is open. All
|
|
149
|
+
# Messages also reports the topic snapshot rendered in its list: an active
|
|
150
|
+
# topic restored later has not yet been viewed.
|
|
151
|
+
def viewing_topic(data)
|
|
152
|
+
return unless @creative_id && current_user
|
|
153
|
+
|
|
154
|
+
topic_id = data["topic_id"] || data[:topic_id]
|
|
155
|
+
@viewing_topic_id = topic_id.present? && Topic.find_by(id: topic_id, creative_id: @creative_id)&.id
|
|
156
|
+
return if topic_id.present? && !@viewing_topic_id
|
|
157
|
+
@rendered_topic_ids = rendered_topic_ids_for(data) unless @viewing_topic_id
|
|
158
|
+
@rendered_legacy_topic = rendered_legacy_topic_for(data) unless @viewing_topic_id
|
|
159
|
+
@viewing_topic_reported = true
|
|
160
|
+
|
|
161
|
+
CommentPresenceStore.set_topic(
|
|
162
|
+
@creative_id,
|
|
163
|
+
current_user.id,
|
|
164
|
+
@viewing_topic_id,
|
|
165
|
+
subscription_id: @presence_subscription_id,
|
|
166
|
+
rendered_topic_ids: @rendered_topic_ids,
|
|
167
|
+
rendered_legacy_topic: @rendered_legacy_topic
|
|
168
|
+
)
|
|
169
|
+
Comment.broadcast_badge(Creative.find(@creative_id), current_user)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The browser keeps this lease alive while its Action Cable subscription is
|
|
173
|
+
# healthy. If the process or connection disappears without #unsubscribed,
|
|
174
|
+
# the lease simply expires and no longer suppresses unread badges.
|
|
175
|
+
def heartbeat
|
|
176
|
+
return unless @creative_id && current_user
|
|
177
|
+
|
|
178
|
+
return if CommentPresenceStore.renew(
|
|
179
|
+
@creative_id,
|
|
180
|
+
current_user.id,
|
|
181
|
+
subscription_id: @presence_subscription_id
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# A frozen browser can resume on an intact WebSocket after its cache lease
|
|
185
|
+
# expires. Recreate this live channel's membership from its last reported
|
|
186
|
+
# rendering context instead of waiting for a topic switch or resubscribe.
|
|
187
|
+
if @viewing_topic_reported
|
|
188
|
+
CommentPresenceStore.set_topic(
|
|
189
|
+
@creative_id,
|
|
190
|
+
current_user.id,
|
|
191
|
+
@viewing_topic_id,
|
|
192
|
+
subscription_id: @presence_subscription_id,
|
|
193
|
+
rendered_topic_ids: @rendered_topic_ids,
|
|
194
|
+
rendered_legacy_topic: @rendered_legacy_topic
|
|
195
|
+
)
|
|
196
|
+
else
|
|
197
|
+
CommentPresenceStore.add(@creative_id, current_user.id, subscription_id: @presence_subscription_id)
|
|
198
|
+
end
|
|
199
|
+
Comment.broadcast_badge(Creative.find(@creative_id), current_user)
|
|
200
|
+
broadcast_presence
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def typing(data)
|
|
88
204
|
return unless @creative_id && current_user
|
|
89
205
|
|
|
206
|
+
topic_id = topic_id_for(data)
|
|
207
|
+
return unless topic_id
|
|
208
|
+
|
|
90
209
|
ActionCable.server.broadcast(
|
|
91
210
|
stream_name,
|
|
92
|
-
{ typing: { id: current_user.id, name: current_user.display_name } }
|
|
211
|
+
{ typing: { id: current_user.id, name: current_user.display_name, topic_id: topic_id } }
|
|
93
212
|
)
|
|
94
213
|
end
|
|
95
214
|
|
|
96
|
-
def stopped_typing
|
|
215
|
+
def stopped_typing(data)
|
|
216
|
+
return unless @creative_id && current_user
|
|
217
|
+
|
|
218
|
+
topic_id = topic_id_for(data)
|
|
219
|
+
return unless topic_id
|
|
220
|
+
|
|
221
|
+
ActionCable.server.broadcast(stream_name, { stop_typing: { id: current_user.id, topic_id: topic_id } })
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The client clears its agent state on every topic switch and asks for the new
|
|
225
|
+
# topic's snapshot. A pending_approval turn has no heartbeat behind it, so this
|
|
226
|
+
# replay is the only thing that can put its indicator and Stop button back.
|
|
227
|
+
def running_agents(data)
|
|
97
228
|
return unless @creative_id && current_user
|
|
98
229
|
|
|
99
|
-
|
|
230
|
+
topic_id = topic_id_for(data)
|
|
231
|
+
return unless topic_id
|
|
232
|
+
|
|
233
|
+
replay_running_agents(topic_id: topic_id)
|
|
100
234
|
end
|
|
101
235
|
|
|
102
236
|
private
|
|
103
237
|
|
|
238
|
+
def rendered_topic_ids_for(data)
|
|
239
|
+
ids = data["rendered_topic_ids"] || data[:rendered_topic_ids]
|
|
240
|
+
return [] unless ids.is_a?(Array)
|
|
241
|
+
|
|
242
|
+
Topic.where(creative_id: @creative_id, id: ids).pluck(:id)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def rendered_legacy_topic_for(data)
|
|
246
|
+
ActiveModel::Type::Boolean.new.cast(data["rendered_legacy_topic"] || data[:rendered_legacy_topic])
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Transmitted to the connection that asked, never broadcast: the stream is per
|
|
250
|
+
# origin and shared by every viewer of it, so publishing one client's snapshot
|
|
251
|
+
# there would re-push agent state to everyone — including viewers sitting on a
|
|
252
|
+
# different topic, whose own state the client would then have to reconcile.
|
|
253
|
+
def replay_running_agents(topic_id: nil)
|
|
254
|
+
self.class
|
|
255
|
+
.running_agent_payloads(@requested_creative_id, topic_id: topic_id)
|
|
256
|
+
.each { |payload| transmit(payload) }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def topic_id_for(data)
|
|
260
|
+
topic_id = data["topic_id"] || data[:topic_id]
|
|
261
|
+
Topic.find_by(id: topic_id, creative_id: @creative_id)&.id
|
|
262
|
+
end
|
|
263
|
+
|
|
104
264
|
def stream_name
|
|
105
265
|
"comments_presence:#{@creative_id}"
|
|
106
266
|
end
|
|
@@ -23,7 +23,13 @@ module Collavre
|
|
|
23
23
|
# badge stale. transmit only reaches this just-confirmed subscriber, so the
|
|
24
24
|
# snapshot can never be sent while no client is listening.
|
|
25
25
|
inbox = Creative.inbox_for(current_user)
|
|
26
|
-
|
|
26
|
+
badge_index = Creatives::CommentBadgeIndex.new(user: current_user)
|
|
27
|
+
badge_index.index([ inbox ])
|
|
28
|
+
snapshot = Comment.inbox_badge_turbo_stream(
|
|
29
|
+
inbox,
|
|
30
|
+
current_user,
|
|
31
|
+
count: badge_index.unread_count_for(inbox)
|
|
32
|
+
)
|
|
27
33
|
transmit(snapshot) if snapshot
|
|
28
34
|
end
|
|
29
35
|
end
|
|
@@ -14,24 +14,17 @@ module Collavre
|
|
|
14
14
|
def count
|
|
15
15
|
return @count if @count
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
else
|
|
21
|
-
# Legacy fallback: InboxItem count
|
|
22
|
-
InboxItem.where(owner: @user, state: "new").count
|
|
23
|
-
end
|
|
17
|
+
return 0 unless @creative && @user
|
|
18
|
+
|
|
19
|
+
unread_count_for_creative
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
private
|
|
27
23
|
|
|
28
24
|
def unread_count_for_creative
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
scope = last_read_id ? visible_comments.where("comments.id > ?", last_read_id) : visible_comments
|
|
34
|
-
scope.count
|
|
25
|
+
badge_index = Creatives::CommentBadgeIndex.new(user: @user)
|
|
26
|
+
badge_index.index([ @creative.effective_origin ])
|
|
27
|
+
badge_index.unread_count_for(@creative.effective_origin)
|
|
35
28
|
end
|
|
36
29
|
end
|
|
37
30
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<div class="progress-filter-group" data-controller="progress-filter">
|
|
1
|
+
<div class="progress-filter-group" data-controller="progress-filter" data-progress-filter-index-path-value="<%= index_path %>" data-progress-filter-on-index-value="<%= on_index? %>">
|
|
2
2
|
<% states.each do |state| %>
|
|
3
|
-
<button class="progress-filter-btn<%= ' active' if current_state.to_s == state[:value].to_s %>" data-action="click->progress-filter#apply" data-progress-filter-target="button" data-progress-filter-filter-param="<%= state[:value] %>"><%= state[:name] %></button>
|
|
3
|
+
<button class="progress-filter-btn<%= ' active' if current_state.to_s == state[:value].to_s %>" data-action="click->progress-filter#apply" data-progress-filter-target="button" data-filter-state="<%= filter_state_key(state[:value]) %>" data-progress-filter-filter-param="<%= state[:value] %>"><%= state[:name] %></button>
|
|
4
4
|
<% end %>
|
|
5
5
|
</div>
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class ProgressFilterComponent < ViewComponent::Base
|
|
3
|
+
# Progress/comment params are only consumed by Creatives::IndexQuery, so the
|
|
4
|
+
# buttons carry the creative index path and whether we are already on it.
|
|
5
|
+
# See app/javascript/lib/utils/filter_navigation.js.
|
|
6
|
+
PROGRESS_VALUES = %w[all complete incomplete].freeze
|
|
7
|
+
|
|
3
8
|
def initialize(current_state:, states: [])
|
|
4
9
|
@current_state = current_state&.to_sym
|
|
5
10
|
@states = states
|
|
6
11
|
end
|
|
7
12
|
|
|
8
13
|
attr_reader :current_state, :states
|
|
14
|
+
|
|
15
|
+
def index_path
|
|
16
|
+
helpers.collavre.creatives_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_index?
|
|
20
|
+
helpers.creative_index_page?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Mirrors the keys syncFilterButtons() derives from the URL, so a frame-only
|
|
24
|
+
# navigation can re-apply the `active` class without a server round trip.
|
|
25
|
+
def filter_state_key(value)
|
|
26
|
+
value = value.to_s
|
|
27
|
+
PROGRESS_VALUES.include?(value) ? "progress:#{value}" : value
|
|
28
|
+
end
|
|
9
29
|
end
|
|
10
30
|
end
|
|
@@ -24,6 +24,7 @@ module Collavre
|
|
|
24
24
|
|
|
25
25
|
# LLM settings
|
|
26
26
|
@llm_request_timeout_seconds = SystemSetting.llm_request_timeout_seconds
|
|
27
|
+
@ai_agent_turn_deadline_seconds = SystemSetting.ai_agent_turn_deadline_seconds
|
|
27
28
|
|
|
28
29
|
# Rate limiting settings
|
|
29
30
|
@password_reset_rate_limit = SystemSetting.password_reset_rate_limit
|
|
@@ -108,6 +109,7 @@ module Collavre
|
|
|
108
109
|
@api_rate_limit = params[:api_rate_limit].to_i.positive? ? params[:api_rate_limit].to_i : SystemSetting::DEFAULT_API_RATE_LIMIT
|
|
109
110
|
@api_rate_period_minutes = params[:api_rate_period_minutes].to_i.positive? ? params[:api_rate_period_minutes].to_i : SystemSetting::DEFAULT_API_RATE_PERIOD_MINUTES
|
|
110
111
|
@llm_request_timeout_seconds = params[:llm_request_timeout_seconds].to_i.positive? ? params[:llm_request_timeout_seconds].to_i : SystemSetting::DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS
|
|
112
|
+
@ai_agent_turn_deadline_seconds = params[:ai_agent_turn_deadline_seconds].to_i.positive? ? params[:ai_agent_turn_deadline_seconds].to_i : SystemSetting::DEFAULT_AI_AGENT_TURN_DEADLINE_SECONDS
|
|
111
113
|
@enabled_auth_providers = params[:auth_providers] || []
|
|
112
114
|
render :index, status: :unprocessable_entity
|
|
113
115
|
end
|
|
@@ -130,7 +132,8 @@ module Collavre
|
|
|
130
132
|
"password_reset_rate_period_minutes" => int_setting(:password_reset_rate_period_minutes, SystemSetting::DEFAULT_PASSWORD_RESET_RATE_PERIOD_MINUTES),
|
|
131
133
|
"api_rate_limit" => int_setting(:api_rate_limit, SystemSetting::DEFAULT_API_RATE_LIMIT),
|
|
132
134
|
"api_rate_period_minutes" => int_setting(:api_rate_period_minutes, SystemSetting::DEFAULT_API_RATE_PERIOD_MINUTES),
|
|
133
|
-
"llm_request_timeout_seconds" => int_setting(:llm_request_timeout_seconds, SystemSetting::DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS, min: 30)
|
|
135
|
+
"llm_request_timeout_seconds" => int_setting(:llm_request_timeout_seconds, SystemSetting::DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS, min: 30),
|
|
136
|
+
"ai_agent_turn_deadline_seconds" => int_setting(:ai_agent_turn_deadline_seconds, SystemSetting::DEFAULT_AI_AGENT_TURN_DEADLINE_SECONDS, min: 60)
|
|
134
137
|
}
|
|
135
138
|
end
|
|
136
139
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class AgentConnectionsController < ApplicationController
|
|
5
|
+
before_action :set_agent_and_workspace
|
|
6
|
+
|
|
7
|
+
def show; end
|
|
8
|
+
|
|
9
|
+
def status
|
|
10
|
+
client = proxy_client
|
|
11
|
+
engines = Array(client.engines["data"])
|
|
12
|
+
engine_rows = engines.map do |engine|
|
|
13
|
+
engine.merge("status" => client.engine_status(engine.fetch("engine")))
|
|
14
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
15
|
+
engine.merge("status" => { "state" => "unknown", "error" => e.message, "code" => e.code })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
render json: {
|
|
19
|
+
engines: engine_rows,
|
|
20
|
+
provision: client.provision_status,
|
|
21
|
+
workspace: {
|
|
22
|
+
credential_id: @workspace.proxy_credential_id,
|
|
23
|
+
workspace_id: @workspace.proxy_workspace_id,
|
|
24
|
+
mode: @agent.agent_gateway.workspace_mode
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
28
|
+
render_proxy_error(e)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_auth_session
|
|
32
|
+
response = proxy_client.create_auth_session(
|
|
33
|
+
params[:engine],
|
|
34
|
+
flow: params[:flow].presence,
|
|
35
|
+
provisioning_url: manifest_url
|
|
36
|
+
)
|
|
37
|
+
render json: response, status: :created
|
|
38
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
39
|
+
render_proxy_error(e)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def auth_session
|
|
43
|
+
render json: proxy_client.auth_session(params[:engine], params[:session_id])
|
|
44
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
45
|
+
render_proxy_error(e)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def submit_auth_session
|
|
49
|
+
base_url = params[:base_url].presence
|
|
50
|
+
validate_custom_gateway_base_url!(base_url) if base_url
|
|
51
|
+
render json: proxy_client.submit_auth_session(
|
|
52
|
+
params[:engine],
|
|
53
|
+
params[:session_id],
|
|
54
|
+
params[:auth_secret],
|
|
55
|
+
base_url: base_url
|
|
56
|
+
)
|
|
57
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
58
|
+
render_proxy_error(e)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cancel_auth_session
|
|
62
|
+
render json: proxy_client.cancel_auth_session(params[:engine], params[:session_id])
|
|
63
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
64
|
+
render_proxy_error(e)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def provision_sync
|
|
68
|
+
render json: sync_provisioning
|
|
69
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
70
|
+
render_proxy_error(e)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def provision_approve
|
|
74
|
+
render json: proxy_client.provision_approve(params[:type], params[:name])
|
|
75
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
76
|
+
render_proxy_error(e)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def provision_delete
|
|
80
|
+
render json: proxy_client.provision_delete(params[:type], params[:name])
|
|
81
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
82
|
+
render_proxy_error(e)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rotate_tokens
|
|
86
|
+
@workspace.rotate_tokens!
|
|
87
|
+
redirect_to agent_connection_user_path(@agent), notice: t("collavre.agent_connections.tokens_rotated")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def set_agent_and_workspace
|
|
93
|
+
@agent = Collavre::User.find(params[:user_id] || params[:id])
|
|
94
|
+
return head :not_found unless @agent.cli_proxy_agent?
|
|
95
|
+
return head :forbidden unless @agent.gateway_accessible_to?(Current.user)
|
|
96
|
+
|
|
97
|
+
@workspace = Collavre::AgentWorkspace.resolve!(agent: @agent, user: Current.user)
|
|
98
|
+
gateway = @workspace.agent_gateway
|
|
99
|
+
if gateway.shared? && Current.user.id != @agent.created_by_id && !Current.user.system_admin?
|
|
100
|
+
head :forbidden
|
|
101
|
+
end
|
|
102
|
+
rescue ArgumentError => error
|
|
103
|
+
raise unless [ "Agent has no gateway", "Agent gateway is inactive" ].include?(error.message)
|
|
104
|
+
|
|
105
|
+
head :service_unavailable
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def proxy_client
|
|
109
|
+
@proxy_client ||= Collavre::CliProxy::Client.new(gateway: @workspace.agent_gateway, workspace: @workspace)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# "Sync now" starts from registration rather than assuming one happened.
|
|
113
|
+
# A manifest URL otherwise only reaches the proxy as a side effect of a
|
|
114
|
+
# login, so a workspace whose registration never landed — an engine
|
|
115
|
+
# authenticated before provisioning existed, a first sync that failed, a
|
|
116
|
+
# proxy that lost its state — could only be repaired by logging in again.
|
|
117
|
+
#
|
|
118
|
+
# Registration is idempotent: re-registering the same URL is a plain
|
|
119
|
+
# re-sync. Two answers mean the proxy will not take a URL from us at all
|
|
120
|
+
# and a plain sync is the whole of what we can do — an operator who pinned
|
|
121
|
+
# the workspace with PROVISION_MANIFEST_URL (409 manifest_url_locked), and
|
|
122
|
+
# a proxy predating the endpoint, whose router answers 404 for it. The one
|
|
123
|
+
# 404 that is not that is provisioning_disabled, where the plain sync would
|
|
124
|
+
# only repeat the same answer and hide which of the two it was.
|
|
125
|
+
def sync_provisioning
|
|
126
|
+
proxy_client.provision_register_manifest(manifest_url)
|
|
127
|
+
rescue Collavre::CliProxy::Client::Error => error
|
|
128
|
+
registration_refused = error.code == "manifest_url_locked" ||
|
|
129
|
+
(error.status == 404 && error.code != "provisioning_disabled")
|
|
130
|
+
raise unless registration_refused
|
|
131
|
+
|
|
132
|
+
proxy_client.provision_sync
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def manifest_url
|
|
136
|
+
"#{request.base_url}#{agent_provision_manifest_path(
|
|
137
|
+
agent_id: @agent.id,
|
|
138
|
+
token: @workspace.manifest_token
|
|
139
|
+
)}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def render_proxy_error(error)
|
|
143
|
+
status = error.status.to_i.between?(400, 599) ? error.status : :bad_gateway
|
|
144
|
+
render json: { error: { message: error.message, code: error.code } }, status: status
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# A custom provider URL is supplied by an agent user but fetched by the
|
|
148
|
+
# proxy with its credentials. Check it here as well as at the proxy boundary
|
|
149
|
+
# so a non-admin user cannot use the login form as an internal-network probe.
|
|
150
|
+
def validate_custom_gateway_base_url!(base_url)
|
|
151
|
+
return if Current.user.system_admin?
|
|
152
|
+
|
|
153
|
+
policy = Collavre::CliProxy::EndpointPolicy.new
|
|
154
|
+
return policy.resolve!(base_url) if policy.safe_literal?(base_url)
|
|
155
|
+
|
|
156
|
+
raise Collavre::CliProxy::EndpointPolicy::UnsafeEndpoint
|
|
157
|
+
rescue Collavre::CliProxy::EndpointPolicy::UnsafeEndpoint
|
|
158
|
+
raise Collavre::CliProxy::Client::Error.new(
|
|
159
|
+
t("collavre.agent_connections.errors.insecure_base_url"),
|
|
160
|
+
status: 422,
|
|
161
|
+
code: "insecure_base_url"
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class AgentGatewaysController < ApplicationController
|
|
5
|
+
before_action :set_gateway, only: %i[edit update destroy check]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@agent_gateways = Current.user.owned_agent_gateways.order(:name)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
@agent_gateway = Current.user.owned_agent_gateways.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@agent_gateway = Current.user.owned_agent_gateways.new(gateway_params)
|
|
17
|
+
if @agent_gateway.save
|
|
18
|
+
redirect_to agent_gateways_path, notice: t("collavre.agent_gateways.created")
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit
|
|
25
|
+
@has_stored_completion_key = @agent_gateway.completion_key.present?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
attributes = gateway_params
|
|
30
|
+
clear_completion_key = ActiveModel::Type::Boolean.new.cast(attributes.delete(:clear_completion_key))
|
|
31
|
+
@has_stored_completion_key = @agent_gateway.completion_key.present?
|
|
32
|
+
@clear_completion_key = clear_completion_key
|
|
33
|
+
|
|
34
|
+
if clear_completion_key
|
|
35
|
+
attributes[:completion_key] = nil
|
|
36
|
+
elsif attributes[:completion_key].blank?
|
|
37
|
+
attributes.delete(:completion_key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
%i[admin_key identity_secret].each do |key|
|
|
41
|
+
attributes.delete(key) if attributes[key].blank?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if @agent_gateway.update(attributes)
|
|
45
|
+
redirect_to agent_gateways_path, notice: t("collavre.agent_gateways.updated")
|
|
46
|
+
else
|
|
47
|
+
render :edit, status: :unprocessable_entity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def destroy
|
|
52
|
+
if @agent_gateway.destroy
|
|
53
|
+
redirect_to agent_gateways_path, notice: t("collavre.agent_gateways.destroyed")
|
|
54
|
+
else
|
|
55
|
+
redirect_to agent_gateways_path, alert: t("collavre.agent_gateways.in_use")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check
|
|
60
|
+
if @agent_gateway.completion_key.present?
|
|
61
|
+
data = connection_check_client(user_key: @agent_gateway.completion_key).engines
|
|
62
|
+
render_connection_check(data, identity: "completion_key")
|
|
63
|
+
else
|
|
64
|
+
check_with_workspace_or_admin
|
|
65
|
+
end
|
|
66
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
67
|
+
return retry_connection_check_with_workspace if e.code == "user_identity_required"
|
|
68
|
+
|
|
69
|
+
render json: { ok: false, error: e.message, code: e.code }, status: :bad_gateway
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def set_gateway
|
|
75
|
+
@agent_gateway = Current.user.owned_agent_gateways.find(params[:id])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def gateway_params
|
|
79
|
+
params.require(:agent_gateway).permit(
|
|
80
|
+
:name, :base_url, :admin_key, :completion_key, :identity_secret,
|
|
81
|
+
:tenant_id, :workspace_mode, :active, :clear_completion_key
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def retry_connection_check_with_workspace
|
|
86
|
+
workspace = connection_check_workspace
|
|
87
|
+
return render_identity_unverified unless workspace
|
|
88
|
+
|
|
89
|
+
data = connection_check_client(workspace: workspace).engines
|
|
90
|
+
render_connection_check(data, identity: "workspace")
|
|
91
|
+
rescue Collavre::CliProxy::Client::Error => e
|
|
92
|
+
render json: { ok: false, error: e.message, code: e.code }, status: :bad_gateway
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_with_workspace_or_admin
|
|
96
|
+
workspace = connection_check_workspace
|
|
97
|
+
return retry_connection_check_with_workspace if workspace
|
|
98
|
+
|
|
99
|
+
data = connection_check_client.engines
|
|
100
|
+
render_identity_unverified(data)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def connection_check_workspace
|
|
104
|
+
scope = @agent_gateway.agent_workspaces.order(:id)
|
|
105
|
+
return scope.find_by(user: Current.user) if @agent_gateway.per_user?
|
|
106
|
+
|
|
107
|
+
scope.joins(:agent).find_by(user_id: nil, users: { created_by_id: Current.user.id })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def connection_check_client(user_key: nil, workspace: nil)
|
|
111
|
+
Collavre::CliProxy::Client.new(gateway: @agent_gateway, user_key: user_key, workspace: workspace)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def render_connection_check(data, identity:)
|
|
115
|
+
render json: {
|
|
116
|
+
ok: true,
|
|
117
|
+
identity_verified: true,
|
|
118
|
+
identity: identity,
|
|
119
|
+
engines: Array(data["data"]).pluck("engine")
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def render_identity_unverified(data = {})
|
|
124
|
+
render json: {
|
|
125
|
+
ok: true,
|
|
126
|
+
identity_verified: false,
|
|
127
|
+
engines: Array(data["data"]).pluck("engine"),
|
|
128
|
+
warning: t("collavre.agent_gateways.identity_unverified")
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|