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
data/app/models/collavre/task.rb
CHANGED
|
@@ -4,8 +4,6 @@ module Collavre
|
|
|
4
4
|
|
|
5
5
|
belongs_to :agent, class_name: "Collavre::User"
|
|
6
6
|
has_many :task_actions, class_name: "Collavre::TaskAction", dependent: :destroy
|
|
7
|
-
belongs_to :parent_task, class_name: "Collavre::Task", optional: true
|
|
8
|
-
has_many :sub_tasks, class_name: "Collavre::Task", foreign_key: :parent_task_id, dependent: :destroy
|
|
9
7
|
belongs_to :creative, class_name: "Collavre::Creative", optional: true
|
|
10
8
|
has_one :reply_comment, class_name: "Collavre::Comment", foreign_key: :task_id, dependent: :nullify
|
|
11
9
|
|
|
@@ -25,6 +23,7 @@ module Collavre
|
|
|
25
23
|
|
|
26
24
|
after_update_commit :check_trigger_loop_completion, if: :trigger_loop_candidate?
|
|
27
25
|
after_update_commit :broadcast_stop_button_removal, if: :became_terminal?
|
|
26
|
+
after_update_commit :restore_undelivered_dispatches, if: :ended_without_delivering?
|
|
28
27
|
|
|
29
28
|
scope :running_for_topic, ->(topic_id, creative_id = nil) {
|
|
30
29
|
rel = where(topic_id: topic_id, status: %w[running delegated])
|
|
@@ -52,6 +51,36 @@ module Collavre
|
|
|
52
51
|
rel
|
|
53
52
|
}
|
|
54
53
|
|
|
54
|
+
# Of the slot-occupying statuses, these have no live worker to run
|
|
55
|
+
# AiAgentJob's ensure-block drain, so whoever cancels one must release the
|
|
56
|
+
# slot itself.
|
|
57
|
+
HELD_SLOT_WITHOUT_WORKER = %w[pending delegated pending_approval].freeze
|
|
58
|
+
|
|
59
|
+
# Work still worth a typing indicator — and the Stop button that only exists
|
|
60
|
+
# inside it: everything occupying_topic_slot counts, plus queued, a waiter
|
|
61
|
+
# not yet promoted. delegated and pending_approval look idle but are not, the
|
|
62
|
+
# job returned holding the slot while it awaits an MCP /reply or a tool
|
|
63
|
+
# approval, and #cancel still accepts both. Kept here rather than in the
|
|
64
|
+
# client so one list decides it.
|
|
65
|
+
ACTIVE_STATUSES = %w[running delegated pending pending_approval queued].freeze
|
|
66
|
+
|
|
67
|
+
def active?
|
|
68
|
+
ACTIVE_STATUSES.include?(status)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Cancellation callers often select an active row before waiting on another
|
|
72
|
+
# request's task lock. Reload under that lock so a completed reply cannot be
|
|
73
|
+
# overwritten by a stale running/delegated instance.
|
|
74
|
+
def cancel_if_active!(statuses: ACTIVE_STATUSES, **attributes)
|
|
75
|
+
with_lock do
|
|
76
|
+
next unless status.in?(statuses)
|
|
77
|
+
|
|
78
|
+
previous_status = status
|
|
79
|
+
update!(attributes.merge(status: "cancelled"))
|
|
80
|
+
previous_status
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
55
84
|
# Check if agent already has an in-flight task triggered by the same comment.
|
|
56
85
|
# Treats "delegated" as in-flight: a Claude Channel task that is waiting on
|
|
57
86
|
# an external MCP reply is still active work — re-dispatching the same
|
|
@@ -77,6 +106,33 @@ module Collavre
|
|
|
77
106
|
broadcast_stop_button_removal if terminal_status?
|
|
78
107
|
end
|
|
79
108
|
|
|
109
|
+
# What a persisted row says about whether this turn ever handed anything
|
|
110
|
+
# over — ended_without_delivering? without the transition.
|
|
111
|
+
#
|
|
112
|
+
# Asked twice: by the callback of a status change, and by
|
|
113
|
+
# RestoreDroppedDispatchesJob of a row it finds afterwards. Written once,
|
|
114
|
+
# because a sweep deciding "terminal" where the callback decides this is how
|
|
115
|
+
# a backstop comes to re-answer comments the turn had already answered.
|
|
116
|
+
def ended_undelivered?
|
|
117
|
+
payload = trigger_event_payload
|
|
118
|
+
# Asked first, so that a row carrying both records is read as undelivered.
|
|
119
|
+
# A resumed turn can legitimately carry both: an earlier attempt handed
|
|
120
|
+
# off, while a later one failed before doing so. DeliveryRecord restores
|
|
121
|
+
# that row but subtracts the comment ids the successful attempts carried,
|
|
122
|
+
# leaving only the later attempt's undelivered comments.
|
|
123
|
+
return true if status == "done" && Orchestration::DeliveryRecord.handoff_failed?(payload)
|
|
124
|
+
|
|
125
|
+
# An ending is undelivered because the payload never reached the provider,
|
|
126
|
+
# not because of the name of the ending. A turn stopped — or broken — after
|
|
127
|
+
# the handoff has been read by the agent along with everything it
|
|
128
|
+
# swallowed, and the product already shows that turn with its partial
|
|
129
|
+
# reply. Only a turn that got far enough to record the handoff is exempt;
|
|
130
|
+
# everything quieter than that still owes its dispatches back.
|
|
131
|
+
return false if Orchestration::DeliveryRecord.handed_off?(payload)
|
|
132
|
+
|
|
133
|
+
status.in?(Orchestration::DeliveryRecord::UNDELIVERED_TERMINAL_STATUSES)
|
|
134
|
+
end
|
|
135
|
+
|
|
80
136
|
private
|
|
81
137
|
|
|
82
138
|
def trigger_loop_candidate?
|
|
@@ -106,6 +162,69 @@ module Collavre
|
|
|
106
162
|
saved_change_to_attribute?("status") && terminal_status?
|
|
107
163
|
end
|
|
108
164
|
|
|
165
|
+
# This turn refused other dispatches on the strength of having read their
|
|
166
|
+
# comments, and then died without answering anything. Those dispatches have
|
|
167
|
+
# to come back — see Orchestration::DeliveryRecord.restore!.
|
|
168
|
+
#
|
|
169
|
+
# Only the status transition is decided here. Whether anything was actually
|
|
170
|
+
# refused is asked inside restore!, off the persisted row: the drop record
|
|
171
|
+
# is written by the refused dispatch in another process, so this object's
|
|
172
|
+
# payload predates it and a check here would read `none` on every turn that
|
|
173
|
+
# dropped something.
|
|
174
|
+
#
|
|
175
|
+
# A status callback rather than a call site: AiAgentJob's rescue and
|
|
176
|
+
# StuckDetector both end a turn this way, and only one of them runs for a
|
|
177
|
+
# process that was killed outright.
|
|
178
|
+
#
|
|
179
|
+
# `done` is an undelivered ending too when the turn recorded that its
|
|
180
|
+
# request never reached the provider: AiClient#chat catches the error and
|
|
181
|
+
# AiAgentJob finishes the task normally, so the status alone cannot say it.
|
|
182
|
+
# Read off this object rather than the row because the flag is written by
|
|
183
|
+
# this turn's own service, in this process, immediately before the status
|
|
184
|
+
# update that fires this callback — unlike DROPPED_KEY, which the refused
|
|
185
|
+
# dispatch writes from another process and restore! therefore re-reads.
|
|
186
|
+
def ended_without_delivering?
|
|
187
|
+
return false unless saved_change_to_attribute?("status")
|
|
188
|
+
return false if ended_while_worker_settles?
|
|
189
|
+
|
|
190
|
+
ended_undelivered?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A turn ended from another process while its worker is still inside the
|
|
194
|
+
# provider call, whose own account of the handoff does not exist yet.
|
|
195
|
+
#
|
|
196
|
+
# Stop is committed from a web request, and StuckDetector can fail a running
|
|
197
|
+
# row from its recurring job. Both can beat AiAgentService's ensure, which
|
|
198
|
+
# writes whether the payload reached the provider only after the call exits.
|
|
199
|
+
# This callback fires before that evidence exists.
|
|
200
|
+
#
|
|
201
|
+
# So it declines, and the question is asked where the turn settles —
|
|
202
|
+
# AiAgentJob's teardown, once the record is written. Behind that,
|
|
203
|
+
# DeliveryRecord.restore_missed! covers an ending with no live worker to
|
|
204
|
+
# settle anything.
|
|
205
|
+
def ended_while_worker_settles?
|
|
206
|
+
return false unless status_before_last_save == "running"
|
|
207
|
+
return true if status == "cancelled"
|
|
208
|
+
|
|
209
|
+
status == "failed" &&
|
|
210
|
+
Orchestration::DeliveryRecord.worker_settling?(trigger_event_payload)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Best effort, deliberately: this runs after the turn's own status is
|
|
214
|
+
# committed, so an exception here propagates back into the update! that
|
|
215
|
+
# ended the turn — and AiAgentJob's rescue answers that by writing `failed`
|
|
216
|
+
# over a turn that had in fact finished and answered. The restore is
|
|
217
|
+
# reconstructible from the row it was given, and
|
|
218
|
+
# DeliveryRecord.restore_missed! is what asks again.
|
|
219
|
+
def restore_undelivered_dispatches
|
|
220
|
+
Orchestration::DeliveryRecord.restore!(self)
|
|
221
|
+
rescue StandardError => e
|
|
222
|
+
Rails.logger.error(
|
|
223
|
+
"[Task] Restore failed for task #{id}: #{e.class}: #{e.message} — " \
|
|
224
|
+
"leaving it to the restore sweep"
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
109
228
|
def terminal_status?
|
|
110
229
|
status.in?(%w[done cancelled failed])
|
|
111
230
|
end
|
|
@@ -18,6 +18,11 @@ module Collavre
|
|
|
18
18
|
has_many :user_creative_preferences_as_last_topic, class_name: "Collavre::UserCreativePreference",
|
|
19
19
|
foreign_key: :last_topic_id, dependent: :nullify, inverse_of: :last_topic
|
|
20
20
|
|
|
21
|
+
# The dependent nullify below clears a user's selected topic without going
|
|
22
|
+
# through the preference controller. Advance the ordering token first so a
|
|
23
|
+
# client can distinguish that deletion from an older empty preference.
|
|
24
|
+
before_destroy :advance_last_topic_preference_revisions, prepend: true
|
|
25
|
+
|
|
21
26
|
# --- Archive scopes ---
|
|
22
27
|
scope :active, -> { where(archived_at: nil) }
|
|
23
28
|
scope :archived, -> { where.not(archived_at: nil) }
|
|
@@ -33,6 +38,48 @@ module Collavre
|
|
|
33
38
|
update!(primary_agent: agent)
|
|
34
39
|
end
|
|
35
40
|
|
|
41
|
+
# A primary agent is an exclusive assignment: Matcher#match_by_primary_agent
|
|
42
|
+
# routes ambient events to it alone and returns [] — silence, not a fallback
|
|
43
|
+
# — when it cannot respond. Routing also requires :feedback on the creative,
|
|
44
|
+
# so pinning an agent that lacks it would disable the topic outright: the
|
|
45
|
+
# pinned agent is not permitted to answer, and every other agent is excluded
|
|
46
|
+
# by the assignment. User.mentionable_for and the agent palette both surface
|
|
47
|
+
# searchable agents regardless of creative access, so this is reachable from
|
|
48
|
+
# plain drag-and-drop or `/topic "name" @agent:`, not just a crafted request.
|
|
49
|
+
#
|
|
50
|
+
# This deliberately mirrors BOTH predicates Matcher applies before it will
|
|
51
|
+
# route to a primary agent — #has_creative_permission? and
|
|
52
|
+
# #eligible_in_inbox? — so "assignable" and "can respond" cannot drift
|
|
53
|
+
# apart. Checking only the permission is not enough: registration grants a
|
|
54
|
+
# Claude Channel agent inbox-wide :feedback, so it passes that check on
|
|
55
|
+
# every inbox topic while Matcher confines it to its own session topic.
|
|
56
|
+
# Pinning one on an ordinary inbox topic would therefore silence the topic
|
|
57
|
+
# for everyone, and it is reachable by plain drag-and-drop (the agent is
|
|
58
|
+
# created_by the user, so the palette lists it) or by `/topic "Main" @agent:`
|
|
59
|
+
# (the inbox share puts it in User.mentionable_for regardless of searchable).
|
|
60
|
+
#
|
|
61
|
+
# Returns nil when assignable, otherwise a symbol naming the reason, so
|
|
62
|
+
# callers can render a message that says which rule was hit.
|
|
63
|
+
#
|
|
64
|
+
# Deliberately NOT enforced inside #set_primary_agent!: SessionProvisioner
|
|
65
|
+
# and Api::V1::AgentsController#register write the pin as session identity
|
|
66
|
+
# before the inbox share exists, and must not be gated on it.
|
|
67
|
+
def self.primary_agent_rejection(creative, agent, topic: nil)
|
|
68
|
+
return :not_an_agent unless agent&.ai_user?
|
|
69
|
+
return :no_creative_access unless creative&.has_permission?(agent, :feedback)
|
|
70
|
+
|
|
71
|
+
# Mirror Matcher#eligible_in_inbox?
|
|
72
|
+
return nil unless creative.inbox?
|
|
73
|
+
return nil unless agent.claude_channel_agent?
|
|
74
|
+
return nil if topic&.session_id.present? && topic.primary_agent_id == agent.id
|
|
75
|
+
|
|
76
|
+
:session_agent_outside_session_topic
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.primary_agent_assignable?(creative, agent, topic: nil)
|
|
80
|
+
primary_agent_rejection(creative, agent, topic: topic).nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
36
83
|
def archived?
|
|
37
84
|
archived_at.present?
|
|
38
85
|
end
|
|
@@ -47,6 +94,10 @@ module Collavre
|
|
|
47
94
|
|
|
48
95
|
private
|
|
49
96
|
|
|
97
|
+
def advance_last_topic_preference_revisions
|
|
98
|
+
user_creative_preferences_as_last_topic.update_all("last_topic_revision = last_topic_revision + 1")
|
|
99
|
+
end
|
|
100
|
+
|
|
50
101
|
def set_default_position
|
|
51
102
|
return if position_changed? && position != 0
|
|
52
103
|
|
data/app/models/collavre/user.rb
CHANGED
|
@@ -29,7 +29,6 @@ module Collavre
|
|
|
29
29
|
has_many :creative_shares_caches, class_name: "Collavre::CreativeSharesCache", dependent: :delete_all
|
|
30
30
|
has_many :shared_creative_shares, class_name: "Collavre::CreativeShare", foreign_key: :shared_by_id,
|
|
31
31
|
dependent: :nullify, inverse_of: :shared_by
|
|
32
|
-
has_many :inbox_items, class_name: "Collavre::InboxItem", foreign_key: :owner_id, dependent: :destroy, inverse_of: :owner
|
|
33
32
|
has_many :invitations, class_name: "Collavre::Invitation", foreign_key: :inviter_id, dependent: :destroy, inverse_of: :inviter
|
|
34
33
|
has_many :activity_logs, class_name: "Collavre::ActivityLog", dependent: :destroy
|
|
35
34
|
has_many :labels, class_name: "Collavre::Label", foreign_key: :owner_id, dependent: :destroy
|
|
@@ -42,6 +41,7 @@ module Collavre
|
|
|
42
41
|
|
|
43
42
|
# Leaf-first destroy to avoid closure_tree find(parent_id) errors
|
|
44
43
|
has_many :creatives, class_name: "Collavre::Creative", dependent: nil
|
|
44
|
+
belongs_to :last_visited_creative, class_name: "Collavre::Creative", optional: true
|
|
45
45
|
before_destroy :destroy_creatives_leaf_first
|
|
46
46
|
|
|
47
47
|
# /compress and /merge summaries are durable recovery artifacts: they replace
|
|
@@ -55,6 +55,16 @@ module Collavre
|
|
|
55
55
|
|
|
56
56
|
belongs_to :creator, class_name: "Collavre::User", foreign_key: "created_by_id", optional: true
|
|
57
57
|
has_many :created_ai_users, class_name: "Collavre::User", foreign_key: "created_by_id", dependent: :destroy
|
|
58
|
+
has_many :created_llm_models,
|
|
59
|
+
class_name: "Collavre::LlmModel",
|
|
60
|
+
foreign_key: :created_by_id,
|
|
61
|
+
dependent: :nullify,
|
|
62
|
+
inverse_of: :creator
|
|
63
|
+
belongs_to :agent_gateway, class_name: "Collavre::AgentGateway", optional: true
|
|
64
|
+
has_many :owned_agent_gateways, class_name: "Collavre::AgentGateway", foreign_key: :owner_id, dependent: :destroy
|
|
65
|
+
has_many :agent_workspaces, class_name: "Collavre::AgentWorkspace", foreign_key: :agent_id, dependent: :destroy
|
|
66
|
+
has_many :personal_agent_workspaces, class_name: "Collavre::AgentWorkspace", foreign_key: :user_id, dependent: :destroy
|
|
67
|
+
after_update :revoke_old_gateway_workspaces, if: :saved_change_to_agent_gateway_id?
|
|
58
68
|
|
|
59
69
|
has_one_attached :avatar
|
|
60
70
|
|
|
@@ -66,15 +76,7 @@ module Collavre
|
|
|
66
76
|
attribute :locale, :string
|
|
67
77
|
attribute :system_admin, :boolean, default: false
|
|
68
78
|
attribute :searchable, :boolean, default: false
|
|
69
|
-
|
|
70
|
-
# Typo correction (2D gating: typing-device AND input-location must both be on).
|
|
71
|
-
attribute :typo_correction_enabled, :boolean, default: true
|
|
72
|
-
attribute :typo_correction_threshold, :integer, default: 80
|
|
73
|
-
attribute :typo_correction_on_soft_keyboard, :boolean, default: true
|
|
74
|
-
attribute :typo_correction_on_voice, :boolean, default: true
|
|
75
|
-
attribute :typo_correction_on_physical_keyboard, :boolean, default: false
|
|
76
|
-
attribute :typo_correction_in_chat, :boolean, default: true
|
|
77
|
-
attribute :typo_correction_in_editor, :boolean, default: false
|
|
79
|
+
attribute :creative_workspace_enabled, :boolean, default: true
|
|
78
80
|
|
|
79
81
|
attribute :google_uid, :string
|
|
80
82
|
attribute :google_access_token, :string
|
|
@@ -88,6 +90,7 @@ module Collavre
|
|
|
88
90
|
attribute :gateway_url, :string
|
|
89
91
|
attribute :tools, :json, default: -> { [] }
|
|
90
92
|
attribute :agent_conf, :string
|
|
93
|
+
attribute :dismissed_notices, :json, default: -> { [] }
|
|
91
94
|
|
|
92
95
|
# Default context settings for AI agents
|
|
93
96
|
AGENT_CONF_DEFAULTS = {
|
|
@@ -160,31 +163,6 @@ module Collavre
|
|
|
160
163
|
end
|
|
161
164
|
end
|
|
162
165
|
|
|
163
|
-
TYPO_CORRECTION_DEVICES = %w[voice soft_keyboard physical_keyboard].freeze
|
|
164
|
-
TYPO_CORRECTION_LOCATIONS = %w[chat editor].freeze
|
|
165
|
-
|
|
166
|
-
# 2D gating: typo correction runs only when the master switch is on AND the
|
|
167
|
-
# originating typing device AND the input location are both enabled. Unknown
|
|
168
|
-
# device/location values are treated as disabled (fail closed).
|
|
169
|
-
def typo_correction_active_for?(device:, location:)
|
|
170
|
-
return false unless typo_correction_enabled
|
|
171
|
-
|
|
172
|
-
device_on = case device.to_s
|
|
173
|
-
when "voice" then typo_correction_on_voice
|
|
174
|
-
when "soft_keyboard" then typo_correction_on_soft_keyboard
|
|
175
|
-
when "physical_keyboard" then typo_correction_on_physical_keyboard
|
|
176
|
-
else false
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
location_on = case location.to_s
|
|
180
|
-
when "chat" then typo_correction_in_chat
|
|
181
|
-
when "editor" then typo_correction_in_editor
|
|
182
|
-
else false
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
device_on && location_on
|
|
186
|
-
end
|
|
187
|
-
|
|
188
166
|
# LLM_VENDOR_OPTIONS is resolved dynamically from the AiClient vendor-option
|
|
189
167
|
# registry so core lists only its built-in providers while vendor engines
|
|
190
168
|
# (e.g. OpenClaw) contribute their own. Resolved lazily via const_missing so
|
|
@@ -200,17 +178,50 @@ module Collavre
|
|
|
200
178
|
SUPPORTED_LLM_MODELS = [
|
|
201
179
|
"gemini-3.1-flash-lite",
|
|
202
180
|
"gemini-1.5-flash",
|
|
203
|
-
"gemini-1.5-pro"
|
|
181
|
+
"gemini-1.5-pro",
|
|
182
|
+
"paperclip/claude_local",
|
|
183
|
+
"paperclip/codex_local"
|
|
204
184
|
].freeze
|
|
205
185
|
|
|
206
186
|
def ai_user?
|
|
207
187
|
llm_vendor.present?
|
|
208
188
|
end
|
|
209
189
|
|
|
190
|
+
def cli_proxy_agent?
|
|
191
|
+
llm_vendor == "cli_proxy" && agent_gateway.present?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def gateway_accessible_to?(user)
|
|
195
|
+
return false unless user
|
|
196
|
+
return true if user.system_admin? || created_by_id == user.id
|
|
197
|
+
return true if user.contact_users.where(id: id).exists?
|
|
198
|
+
|
|
199
|
+
agent_creative_ids = Collavre::Creative.where(user_id: id).pluck(:id)
|
|
200
|
+
agent_creative_ids.concat(
|
|
201
|
+
Collavre::CreativeSharesCache.where(user_id: [ id, nil ]).distinct.pluck(:creative_id)
|
|
202
|
+
)
|
|
203
|
+
agent_permissions = Collavre::Creatives::PermissionFilter.new(user: self)
|
|
204
|
+
agent_creative_ids = agent_permissions.readable_ids(agent_creative_ids, min_permission: :feedback)
|
|
205
|
+
Collavre::Creatives::PermissionFilter.new(user: user)
|
|
206
|
+
.readable_ids(agent_creative_ids)
|
|
207
|
+
.any?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def revoke_old_gateway_workspaces
|
|
211
|
+
old_gateway_id = agent_gateway_id_before_last_save
|
|
212
|
+
return unless old_gateway_id
|
|
213
|
+
|
|
214
|
+
agent_workspaces.where(agent_gateway_id: old_gateway_id).destroy_all
|
|
215
|
+
end
|
|
216
|
+
|
|
210
217
|
def claude_channel_agent?
|
|
211
218
|
llm_model == "claude-code"
|
|
212
219
|
end
|
|
213
220
|
|
|
221
|
+
def claude_channel_online?
|
|
222
|
+
claude_channel_agent? && AgentSubscription.live.where(agent_id: id).exists?
|
|
223
|
+
end
|
|
224
|
+
|
|
214
225
|
scope :ai_agents, -> { where.not(llm_vendor: [ nil, "" ]) }
|
|
215
226
|
|
|
216
227
|
def self.accessible_ai_agents_for(user)
|
|
@@ -239,16 +250,65 @@ module Collavre
|
|
|
239
250
|
validates :email, presence: true, uniqueness: true,
|
|
240
251
|
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
241
252
|
validates :name, presence: true
|
|
253
|
+
validates :llm_vendor,
|
|
254
|
+
length: { maximum: Collavre::LlmModel::MAX_VENDOR_LENGTH },
|
|
255
|
+
if: :will_save_change_to_llm_vendor?
|
|
256
|
+
validates :llm_model,
|
|
257
|
+
length: { maximum: Collavre::LlmModel::MAX_NAME_LENGTH },
|
|
258
|
+
if: :will_save_change_to_llm_model?
|
|
259
|
+
validate :cli_proxy_gateway_belongs_to_creator
|
|
260
|
+
around_save :serialize_cli_proxy_gateway_assignment
|
|
261
|
+
before_save :validate_cli_proxy_gateway_assignment_under_lock
|
|
242
262
|
validate :theme_accessibility
|
|
243
263
|
validate :password_meets_minimum_length
|
|
244
264
|
validates :timezone,
|
|
245
265
|
inclusion: { in: ActiveSupport::TimeZone.all.map { |z| z.tzinfo.identifier } },
|
|
246
266
|
allow_nil: true
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
267
|
+
def cli_proxy_gateway_belongs_to_creator
|
|
268
|
+
return unless llm_vendor == "cli_proxy"
|
|
269
|
+
|
|
270
|
+
validate_cli_proxy_gateway(agent_gateway)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# See AgentGateway#serialize_completion_key_removal. This repeats the
|
|
274
|
+
# gateway checks immediately before writing the agent while holding the
|
|
275
|
+
# same row lock used by a completion-key removal.
|
|
276
|
+
def serialize_cli_proxy_gateway_assignment
|
|
277
|
+
return yield unless llm_vendor == "cli_proxy" && agent_gateway_id.present?
|
|
278
|
+
|
|
279
|
+
gateway = AgentGateway.find_by(id: agent_gateway_id)
|
|
280
|
+
return yield unless gateway
|
|
281
|
+
|
|
282
|
+
gateway.with_lock do
|
|
283
|
+
begin
|
|
284
|
+
@cli_proxy_gateway_assignment_lock = gateway
|
|
285
|
+
yield
|
|
286
|
+
ensure
|
|
287
|
+
@cli_proxy_gateway_assignment_lock = nil
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def validate_cli_proxy_gateway_assignment_under_lock
|
|
293
|
+
gateway = @cli_proxy_gateway_assignment_lock
|
|
294
|
+
return unless gateway
|
|
295
|
+
|
|
296
|
+
error_count = errors.count
|
|
297
|
+
validate_cli_proxy_gateway(gateway)
|
|
298
|
+
throw :abort if errors.count > error_count
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def validate_cli_proxy_gateway(gateway)
|
|
302
|
+
if gateway.nil?
|
|
303
|
+
errors.add(:agent_gateway, :blank)
|
|
304
|
+
elsif gateway.owner_id != created_by_id
|
|
305
|
+
errors.add(:agent_gateway, :invalid)
|
|
306
|
+
elsif !gateway.chat_capable?
|
|
307
|
+
errors.add(:agent_gateway, :completion_key_required)
|
|
308
|
+
elsif gateway.identity_secret.blank? && (gateway.per_user? || gateway.agents.where.not(id: id).exists?)
|
|
309
|
+
errors.add(:agent_gateway, :identity_required)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
252
312
|
|
|
253
313
|
generates_token_for :email_verification, expires_in: 1.day do
|
|
254
314
|
email
|
|
@@ -6,7 +6,9 @@ module Collavre
|
|
|
6
6
|
belongs_to :user, class_name: Collavre.configuration.user_class_name
|
|
7
7
|
belongs_to :last_topic, class_name: "Collavre::Topic", optional: true
|
|
8
8
|
|
|
9
|
-
validates :expanded_status, presence: true, unless:
|
|
9
|
+
validates :expanded_status, presence: true, unless: -> {
|
|
10
|
+
last_topic_id? || last_topic_revision.to_i.positive? || last_topic_save_fence_issued.to_i.positive?
|
|
11
|
+
}
|
|
10
12
|
validates :creative_id, uniqueness: { scope: :user_id }, allow_nil: true
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "zlib"
|
|
7
|
+
|
|
8
|
+
module Collavre
|
|
9
|
+
module AgentProvisioning
|
|
10
|
+
class Archive
|
|
11
|
+
BLOCK_SIZE = 512
|
|
12
|
+
MAX_FILE_SIZE = 1.megabyte
|
|
13
|
+
MAX_ARCHIVE_SIZE = 10.megabytes
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def collavre_skill
|
|
17
|
+
@collavre_skill ||= build_from_directory(Collavre::Engine.root.join("skills/collavre"))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def workspace_config(workspace, base_url:)
|
|
21
|
+
json = JSON.pretty_generate(workspace.config_payload(base_url: base_url)) << "\n"
|
|
22
|
+
build({ "config.json" => { content: json, mode: 0o600 } })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sha256(bytes)
|
|
26
|
+
Digest::SHA256.hexdigest(bytes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_from_directory(root)
|
|
32
|
+
raise ArgumentError, "Provisioning source directory does not exist: #{root}" unless root.directory?
|
|
33
|
+
|
|
34
|
+
files = Dir.glob(root.join("**/*"), File::FNM_DOTMATCH).filter_map do |path|
|
|
35
|
+
next unless File.file?(path)
|
|
36
|
+
|
|
37
|
+
relative = Pathname(path).relative_path_from(root).to_s
|
|
38
|
+
mode = File.executable?(path) ? 0o755 : 0o644
|
|
39
|
+
[ relative, { content: File.binread(path), mode: mode } ]
|
|
40
|
+
end.to_h
|
|
41
|
+
raise ArgumentError, "Provisioning source directory is empty: #{root}" if files.empty?
|
|
42
|
+
|
|
43
|
+
build(files)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build(files)
|
|
47
|
+
tar = +"".b
|
|
48
|
+
total_size = 0
|
|
49
|
+
files.sort.each do |name, entry|
|
|
50
|
+
content = entry.fetch(:content).to_s.b
|
|
51
|
+
raise ArgumentError, "Provisioning file is too large: #{name}" if content.bytesize > MAX_FILE_SIZE
|
|
52
|
+
raise ArgumentError, "Provisioning file is binary: #{name}" if content.include?("\0")
|
|
53
|
+
|
|
54
|
+
total_size += content.bytesize
|
|
55
|
+
raise ArgumentError, "Provisioning archive expands beyond the limit" if total_size > MAX_ARCHIVE_SIZE
|
|
56
|
+
|
|
57
|
+
tar << tar_header(name, mode: entry.fetch(:mode), size: content.bytesize)
|
|
58
|
+
tar << content
|
|
59
|
+
tar << "\0" * ((BLOCK_SIZE - (content.bytesize % BLOCK_SIZE)) % BLOCK_SIZE)
|
|
60
|
+
end
|
|
61
|
+
tar << "\0" * (BLOCK_SIZE * 2)
|
|
62
|
+
|
|
63
|
+
io = StringIO.new(+"".b)
|
|
64
|
+
gzip = Zlib::GzipWriter.new(io, Zlib::BEST_COMPRESSION)
|
|
65
|
+
gzip.mtime = 0
|
|
66
|
+
gzip.write(tar)
|
|
67
|
+
gzip.close
|
|
68
|
+
bytes = io.string
|
|
69
|
+
raise ArgumentError, "Provisioning archive is too large" if bytes.bytesize > MAX_ARCHIVE_SIZE
|
|
70
|
+
|
|
71
|
+
bytes.freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tar_header(name, mode:, size:)
|
|
75
|
+
raise ArgumentError, "Provisioning path is too long: #{name}" if name.bytesize > 100
|
|
76
|
+
|
|
77
|
+
header = +"\0" * BLOCK_SIZE
|
|
78
|
+
write_field(header, 0, 100, name)
|
|
79
|
+
write_octal(header, 100, 8, mode)
|
|
80
|
+
write_octal(header, 108, 8, 0)
|
|
81
|
+
write_octal(header, 116, 8, 0)
|
|
82
|
+
write_octal(header, 124, 12, size)
|
|
83
|
+
write_octal(header, 136, 12, 0)
|
|
84
|
+
write_field(header, 148, 8, " " * 8)
|
|
85
|
+
write_field(header, 156, 1, "0")
|
|
86
|
+
write_field(header, 257, 6, "ustar\0")
|
|
87
|
+
write_field(header, 263, 2, "00")
|
|
88
|
+
write_field(header, 265, 32, "root")
|
|
89
|
+
write_field(header, 297, 32, "root")
|
|
90
|
+
checksum = header.bytes.sum
|
|
91
|
+
write_field(header, 148, 8, format("%06o\0 ", checksum))
|
|
92
|
+
header
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def write_octal(buffer, offset, length, value)
|
|
96
|
+
write_field(buffer, offset, length, format("%0#{length - 1}o\0", value))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def write_field(buffer, offset, length, value)
|
|
100
|
+
value = value.to_s.b
|
|
101
|
+
raise ArgumentError, "Tar field overflow" if value.bytesize > length
|
|
102
|
+
|
|
103
|
+
buffer[offset, length] = value.ljust(length, "\0")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -6,10 +6,11 @@ module Collavre
|
|
|
6
6
|
# Detects mentioned agents, records interactions for loop prevention,
|
|
7
7
|
# and dispatches system events for downstream processing.
|
|
8
8
|
class A2aDispatcher
|
|
9
|
-
def initialize(agent:, reply_comment:, context:)
|
|
9
|
+
def initialize(agent:, reply_comment:, context:, workspace_user: nil)
|
|
10
10
|
@agent = agent
|
|
11
11
|
@reply_comment = reply_comment
|
|
12
12
|
@context = context
|
|
13
|
+
@workspace_user = workspace_user
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
# Dispatch A2A events if the response mentions any AI agents
|
|
@@ -53,7 +54,7 @@ module Collavre
|
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def dispatch_event(creative)
|
|
56
|
-
|
|
57
|
+
payload = {
|
|
57
58
|
comment: {
|
|
58
59
|
id: @reply_comment.id,
|
|
59
60
|
content: @reply_comment.content,
|
|
@@ -65,7 +66,19 @@ module Collavre
|
|
|
65
66
|
},
|
|
66
67
|
topic: { id: @reply_comment.topic_id },
|
|
67
68
|
chat: { content: @reply_comment.content }
|
|
68
|
-
}
|
|
69
|
+
}
|
|
70
|
+
# Always carry the resolved principal, including an explicit nil. Nil
|
|
71
|
+
# means the current anchor could not prove a human identity; omitting the
|
|
72
|
+
# key would let a downstream per-user CLI Proxy agent fall back to its
|
|
73
|
+
# creator and execute with that person's workspace credentials.
|
|
74
|
+
payload[:workspace_user_id] = @workspace_user&.id
|
|
75
|
+
|
|
76
|
+
SystemEvents::Dispatcher.dispatch(
|
|
77
|
+
"comment_created",
|
|
78
|
+
payload,
|
|
79
|
+
source: "a2a",
|
|
80
|
+
parent: SystemEvents::Envelope.in(@context)
|
|
81
|
+
)
|
|
69
82
|
end
|
|
70
83
|
end
|
|
71
84
|
end
|