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
|
@@ -1,60 +1,244 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class UserCreativePreferencesController < ApplicationController
|
|
3
|
+
MAX_LAST_TOPIC_SAVE_SESSIONS = 32
|
|
4
|
+
# A concurrent collapse can delete the row between the lookup and the lock.
|
|
5
|
+
# Retrying twice covers that race; beyond it the row is not coming back.
|
|
6
|
+
MAX_PREFERENCE_LOCK_ATTEMPTS = 3
|
|
7
|
+
|
|
3
8
|
def toggle
|
|
4
9
|
creative_id = params[:creative_id]
|
|
5
10
|
node_id = params[:node_id].to_s
|
|
6
11
|
expanded = ActiveModel::Type::Boolean.new.cast(params[:expanded])
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
with_preference(creative_id) do |record|
|
|
14
|
+
state = record.expanded_status || {}
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
if expanded
|
|
17
|
+
state[node_id] = true
|
|
18
|
+
else
|
|
19
|
+
state.delete(node_id)
|
|
20
|
+
end
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
record.expanded_status = state
|
|
23
|
+
if empty_preference?(record, state)
|
|
24
|
+
record.destroy!
|
|
25
|
+
else
|
|
26
|
+
record.save!
|
|
27
|
+
end
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
render json: { success: true }
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def update_last_topic
|
|
34
|
+
return issue_last_topic_save_fence if request.post?
|
|
35
|
+
|
|
28
36
|
creative = Creative.find(params[:creative_id]).effective_origin
|
|
29
37
|
|
|
30
|
-
unless
|
|
31
|
-
|
|
38
|
+
return render_forbidden unless readable?(creative)
|
|
39
|
+
|
|
40
|
+
result = persist_last_topic_selection(creative)
|
|
41
|
+
return render_invalid_topic unless result
|
|
42
|
+
|
|
43
|
+
record, saved = result
|
|
44
|
+
broadcast_last_topic(creative, record) if saved
|
|
45
|
+
|
|
46
|
+
render json: {
|
|
47
|
+
success: saved,
|
|
48
|
+
stale_last_topic_save: !saved,
|
|
49
|
+
last_topic_revision: [ record.id, record.last_topic_revision ]
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Issue this before sending the PATCH so an older request that is still
|
|
54
|
+
# running can never commit after a newer selection. The two counters are
|
|
55
|
+
# deliberately global to this user/creative preference, rather than one
|
|
56
|
+
# per controller session, so their storage stays bounded.
|
|
57
|
+
def issue_last_topic_save_fence
|
|
58
|
+
creative = Creative.find(params[:creative_id]).effective_origin
|
|
59
|
+
|
|
60
|
+
return render_forbidden unless readable?(creative)
|
|
61
|
+
|
|
62
|
+
fence = with_preference(creative.id) do |record|
|
|
63
|
+
record.last_topic_save_fence_issued = [
|
|
64
|
+
record.last_topic_save_fence_issued.to_i,
|
|
65
|
+
record.last_topic_save_fence_applied.to_i
|
|
66
|
+
].max + 1
|
|
67
|
+
record.save!
|
|
68
|
+
record.last_topic_save_fence_issued
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
render json: { last_topic_save_fence: fence }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def readable?(creative)
|
|
77
|
+
creative.has_permission?(Current.user, :read) || creative.user == Current.user
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def empty_preference?(record, state)
|
|
81
|
+
state.empty? && record.last_topic_id.nil? && record.last_topic_revision.to_i.zero? &&
|
|
82
|
+
record.last_topic_save_fence_issued.to_i.zero? && record.last_topic_save_fence_applied.to_i.zero?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def persist_last_topic_selection(creative)
|
|
86
|
+
topic_id = params[:last_topic_id].presence
|
|
87
|
+
return persist_last_topic(creative) unless topic_id
|
|
88
|
+
|
|
89
|
+
Topic.transaction do
|
|
90
|
+
topic = Topic.lock.find_by(id: topic_id)
|
|
91
|
+
next unless topic&.creative_id == creative.id
|
|
92
|
+
|
|
93
|
+
persist_last_topic(creative)
|
|
32
94
|
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def persist_last_topic(creative)
|
|
98
|
+
with_preference(creative.id) do |record|
|
|
99
|
+
if stale_last_topic_save?(record)
|
|
100
|
+
next [ record, false ]
|
|
101
|
+
end
|
|
33
102
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
103
|
+
record.expanded_status ||= {}
|
|
104
|
+
record.last_topic_id = params[:last_topic_id].presence
|
|
105
|
+
record.last_topic_revision = record.last_topic_revision.to_i + 1
|
|
106
|
+
assign_last_topic_save_order(record)
|
|
107
|
+
# Retain a cleared preference after it has participated in last-topic
|
|
108
|
+
# ordering. Its revision lets a reopened client distinguish a newer
|
|
109
|
+
# Main selection from the empty snapshot that preceded an in-flight save.
|
|
110
|
+
if empty_preference?(record, record.expanded_status)
|
|
111
|
+
record.destroy!
|
|
112
|
+
else
|
|
113
|
+
record.save!
|
|
38
114
|
end
|
|
115
|
+
[ record, true ]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The client id carries a controller session, a monotonically increasing
|
|
120
|
+
# sequence, and an echo nonce. Older clients still send arbitrary ids, so
|
|
121
|
+
# only recognized ordering ids take this fencing path.
|
|
122
|
+
def stale_last_topic_save?(record)
|
|
123
|
+
fence = last_topic_save_fence
|
|
124
|
+
if fence
|
|
125
|
+
return true if fence > record.last_topic_save_fence_issued.to_i
|
|
126
|
+
|
|
127
|
+
return record.last_topic_save_fence_applied.to_i >= fence
|
|
39
128
|
end
|
|
40
129
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
130
|
+
session_id, sequence = last_topic_save_order
|
|
131
|
+
return false unless session_id.present?
|
|
132
|
+
|
|
133
|
+
last_topic_save_sequences(record).fetch(session_id, 0).to_i >= sequence
|
|
134
|
+
end
|
|
44
135
|
|
|
45
|
-
|
|
46
|
-
|
|
136
|
+
def assign_last_topic_save_order(record)
|
|
137
|
+
fence = last_topic_save_fence
|
|
138
|
+
if fence
|
|
139
|
+
record.last_topic_save_fence_applied = fence
|
|
47
140
|
else
|
|
48
|
-
|
|
141
|
+
# Any unfenced PATCH can come from a client running the previous bundle.
|
|
142
|
+
# It supersedes every fence issued before it, so retire them before a
|
|
143
|
+
# delayed fenced save can overwrite it.
|
|
144
|
+
record.last_topic_save_fence_applied = record.last_topic_save_fence_issued.to_i
|
|
49
145
|
end
|
|
50
146
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
147
|
+
# Fenced saves must update this too. A same-session fallback PATCH can
|
|
148
|
+
# outlive an earlier request and must remain ordered behind it.
|
|
149
|
+
session_id, sequence = last_topic_save_order
|
|
150
|
+
return unless session_id.present?
|
|
56
151
|
|
|
57
|
-
|
|
152
|
+
sequences = last_topic_save_sequences(record)
|
|
153
|
+
# Keep recent sessions in insertion order so a preference cannot retain
|
|
154
|
+
# an unbounded history of tabs that have already gone away.
|
|
155
|
+
sequences.delete(session_id)
|
|
156
|
+
sequences[session_id] = sequence
|
|
157
|
+
sequences.shift while sequences.size > MAX_LAST_TOPIC_SAVE_SESSIONS
|
|
158
|
+
|
|
159
|
+
record.last_topic_save_sequences = sequences
|
|
160
|
+
record.last_topic_save_session_id = session_id
|
|
161
|
+
record.last_topic_save_sequence = sequence
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# insert_all uses the unique preference key as the first-insert fence.
|
|
165
|
+
# A row lock alone cannot serialize two requests that both see no row.
|
|
166
|
+
def preference_for(creative_id)
|
|
167
|
+
now = Time.current
|
|
168
|
+
attributes = { creative_id: creative_id, user_id: Current.user.id, expanded_status: {}, created_at: now, updated_at: now }
|
|
169
|
+
UserCreativePreference.insert_all([ attributes ], unique_by: :index_user_creative_preferences_on_creative_id_and_user_id)
|
|
170
|
+
UserCreativePreference.find_by!(creative_id: creative_id, user_id: Current.user.id)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A collapse can remove an empty row after it is found but before with_lock
|
|
174
|
+
# reloads it. Reacquire by the unique preference key so every save path
|
|
175
|
+
# shares the same first-insert and deletion-race handling.
|
|
176
|
+
#
|
|
177
|
+
# Only the lookup/lock window is retried, and only a bounded number of
|
|
178
|
+
# times: once the row is locked the block owns the transaction, so a
|
|
179
|
+
# RecordNotFound it raises itself must propagate rather than replay the
|
|
180
|
+
# block, and a row that keeps vanishing must surface instead of spinning.
|
|
181
|
+
def with_preference(creative_id)
|
|
182
|
+
attempts = 0
|
|
183
|
+
locked = false
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
record = preference_for(creative_id)
|
|
187
|
+
record.with_lock do
|
|
188
|
+
locked = true
|
|
189
|
+
yield record
|
|
190
|
+
end
|
|
191
|
+
rescue ActiveRecord::RecordNotFound
|
|
192
|
+
raise if locked
|
|
193
|
+
|
|
194
|
+
attempts += 1
|
|
195
|
+
raise if attempts >= MAX_PREFERENCE_LOCK_ATTEMPTS
|
|
196
|
+
|
|
197
|
+
retry
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Preserve the legacy single-session columns while a rolling deploy may
|
|
202
|
+
# still have records written by the preceding application version.
|
|
203
|
+
def last_topic_save_sequences(record)
|
|
204
|
+
sequences = record.last_topic_save_sequences || {}
|
|
205
|
+
legacy_session_id = record.last_topic_save_session_id
|
|
206
|
+
return sequences unless legacy_session_id.present?
|
|
207
|
+
|
|
208
|
+
legacy_sequence = record.last_topic_save_sequence.to_i
|
|
209
|
+
sequences.merge(legacy_session_id => [ sequences.fetch(legacy_session_id, 0).to_i, legacy_sequence ].max)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def last_topic_save_order
|
|
213
|
+
match = params[:client_id].to_s.match(/\A([A-Za-z0-9-]+)\.([1-9]\d*)\.[A-Za-z0-9-]+\z/)
|
|
214
|
+
return [ nil, nil ] unless match
|
|
215
|
+
|
|
216
|
+
[ match[1], match[2].to_i ]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def last_topic_save_fence
|
|
220
|
+
value = params[:last_topic_save_fence].to_s
|
|
221
|
+
return unless value.match?(/\A[1-9]\d*\z/)
|
|
222
|
+
|
|
223
|
+
value.to_i
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def broadcast_last_topic(creative, record)
|
|
227
|
+
payload = {
|
|
228
|
+
action: "last_topic_changed",
|
|
229
|
+
last_topic_id: record.last_topic_id,
|
|
230
|
+
last_topic_revision: [ record.id, record.last_topic_revision ],
|
|
231
|
+
client_id: params[:client_id].presence
|
|
232
|
+
}
|
|
233
|
+
TopicsChannel.broadcast_to("user_#{Current.user.id}_creative_#{creative.id}", payload)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def render_forbidden
|
|
237
|
+
render json: { error: I18n.t("collavre.user_creative_preferences.no_permission") }, status: :forbidden
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def render_invalid_topic
|
|
241
|
+
render json: { error: I18n.t("collavre.user_creative_preferences.invalid_topic") }, status: :unprocessable_entity
|
|
58
242
|
end
|
|
59
243
|
end
|
|
60
244
|
end
|
|
@@ -47,9 +47,7 @@ module Collavre
|
|
|
47
47
|
visible_scope = @creative.comments.visible_to(Current.user)
|
|
48
48
|
comments = visible_scope.where(id: comment_ids).to_a
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
render json: { error: I18n.t("collavre.comments.batch_delete_not_found") }, status: :not_found and return
|
|
52
|
-
end
|
|
50
|
+
return unless valid_merge_selection?(comments, comment_ids)
|
|
53
51
|
|
|
54
52
|
# All comments must belong to the current user or their AI agents
|
|
55
53
|
my_ai_agent_ids = Current.user.created_ai_users.pluck(:id)
|
|
@@ -88,6 +86,20 @@ module Collavre
|
|
|
88
86
|
rescue ActiveRecord::RecordInvalid => e
|
|
89
87
|
render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity
|
|
90
88
|
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def valid_merge_selection?(comments, comment_ids)
|
|
93
|
+
if comments.length != comment_ids.length
|
|
94
|
+
render json: { error: I18n.t("collavre.comments.batch_delete_not_found") }, status: :not_found
|
|
95
|
+
false
|
|
96
|
+
elsif comments.map(&:topic_id).uniq.size != 1
|
|
97
|
+
render json: { error: I18n.t("collavre.comments.merge.same_topic_required") }, status: :unprocessable_entity
|
|
98
|
+
false
|
|
99
|
+
else
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
end
|
|
91
103
|
end
|
|
92
104
|
end
|
|
93
105
|
end
|
|
@@ -36,7 +36,7 @@ module Collavre
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def build_convert_system_message(creative)
|
|
39
|
-
title =
|
|
39
|
+
title = Collavre::HtmlText.markdown_label(creative.description)
|
|
40
40
|
title = I18n.t("collavre.comments.convert_system_message_default_title") if title.blank?
|
|
41
41
|
url = creative_path(creative)
|
|
42
42
|
I18n.t("collavre.comments.convert_system_message", title: title, url: url)
|
|
@@ -10,6 +10,8 @@ module Collavre
|
|
|
10
10
|
|
|
11
11
|
def new_ai
|
|
12
12
|
@available_tools = load_available_tools
|
|
13
|
+
@llm_models = Collavre::LlmModel.suggestions
|
|
14
|
+
@agent_gateways = chat_capable_agent_gateways(Current.user)
|
|
13
15
|
|
|
14
16
|
if params[:copy_from].present?
|
|
15
17
|
source = Collavre::User.find_by(id: params[:copy_from])
|
|
@@ -34,6 +36,7 @@ module Collavre
|
|
|
34
36
|
llm_model: params[:llm_model],
|
|
35
37
|
llm_api_key: params[:llm_api_key],
|
|
36
38
|
gateway_url: params[:gateway_url],
|
|
39
|
+
agent_gateway: selected_agent_gateway,
|
|
37
40
|
tools: params[:tools] || [],
|
|
38
41
|
searchable: searchable,
|
|
39
42
|
email_verified_at: Time.current,
|
|
@@ -42,35 +45,122 @@ module Collavre
|
|
|
42
45
|
)
|
|
43
46
|
@user.agent_conf = params[:agent_conf] if @user.respond_to?(:agent_conf=) && params[:agent_conf].present?
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
saved = Collavre::User.transaction do
|
|
49
|
+
next false unless @user.save
|
|
50
|
+
|
|
51
|
+
remember_llm_model(@user)
|
|
46
52
|
Collavre::Contact.ensure(user: Current.user, contact_user: @user)
|
|
47
53
|
share_ai_agent_to_creative(@user, params[:creative_id])
|
|
48
|
-
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if saved
|
|
58
|
+
redirect_to current_user_contacts_path, notice: I18n.t("collavre.users.create_ai.success")
|
|
49
59
|
else
|
|
50
60
|
flash.now[:alert] = @user.errors.full_messages.to_sentence
|
|
51
61
|
@available_tools = load_available_tools
|
|
62
|
+
@llm_models = Collavre::LlmModel.suggestions
|
|
63
|
+
@agent_gateways = chat_capable_agent_gateways(Current.user)
|
|
52
64
|
render :new_ai, status: :unprocessable_entity
|
|
53
65
|
end
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
def edit_ai
|
|
57
69
|
@available_tools = load_available_tools
|
|
70
|
+
@llm_models = Collavre::LlmModel.suggestions
|
|
71
|
+
@agent_gateways = editable_agent_gateways(@user)
|
|
72
|
+
@has_stored_llm_api_key = @user.llm_api_key.present?
|
|
73
|
+
@return_to = safe_return_to(params[:return_to].presence || request.referer)
|
|
58
74
|
end
|
|
59
75
|
|
|
60
76
|
def update_ai
|
|
61
|
-
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_vendor, :llm_model, :llm_api_key, :gateway_url, :searchable, :routing_expression, :agent_conf, tools: [])
|
|
77
|
+
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_vendor, :llm_model, :llm_api_key, :clear_llm_api_key, :gateway_url, :agent_gateway_id, :searchable, :routing_expression, :agent_conf, tools: [])
|
|
78
|
+
effective_vendor = ai_params[:llm_vendor].presence || @user.llm_vendor
|
|
79
|
+
if effective_vendor == "cli_proxy" && ai_params.key?(:agent_gateway_id)
|
|
80
|
+
gateways = gateway_owner_for(@user).owned_agent_gateways
|
|
81
|
+
gateway = if ai_params[:agent_gateway_id].to_s == @user.agent_gateway_id.to_s
|
|
82
|
+
gateways.find_by(id: ai_params[:agent_gateway_id])
|
|
83
|
+
else
|
|
84
|
+
gateways.active.find_by(id: ai_params[:agent_gateway_id])
|
|
85
|
+
end
|
|
86
|
+
ai_params[:agent_gateway_id] = gateway&.id
|
|
87
|
+
elsif ai_params.key?(:llm_vendor)
|
|
88
|
+
ai_params[:agent_gateway_id] = nil
|
|
89
|
+
end
|
|
90
|
+
clear_llm_api_key = ActiveModel::Type::Boolean.new.cast(ai_params.delete(:clear_llm_api_key))
|
|
91
|
+
@has_stored_llm_api_key = @user.llm_api_key.present?
|
|
92
|
+
@clear_llm_api_key = clear_llm_api_key
|
|
93
|
+
|
|
94
|
+
if clear_llm_api_key
|
|
95
|
+
ai_params[:llm_api_key] = nil
|
|
96
|
+
elsif ai_params[:llm_api_key].blank?
|
|
97
|
+
ai_params.delete(:llm_api_key)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
updated = Collavre::User.transaction do
|
|
101
|
+
next false unless @user.update(ai_params)
|
|
102
|
+
|
|
103
|
+
if @user.saved_change_to_llm_vendor? || @user.saved_change_to_llm_model?
|
|
104
|
+
remember_llm_model(@user)
|
|
105
|
+
end
|
|
106
|
+
true
|
|
107
|
+
end
|
|
62
108
|
|
|
63
|
-
if
|
|
64
|
-
redirect_to
|
|
109
|
+
if updated
|
|
110
|
+
redirect_to update_ai_destination, notice: I18n.t("collavre.users.update_ai.success")
|
|
65
111
|
else
|
|
66
|
-
|
|
67
|
-
flash.now[:alert] = @user.errors.full_messages.to_sentence
|
|
68
|
-
render :edit_ai, status: :unprocessable_entity
|
|
112
|
+
render_ai_edit_failure
|
|
69
113
|
end
|
|
70
114
|
end
|
|
71
115
|
|
|
72
116
|
private
|
|
73
117
|
|
|
118
|
+
def current_user_contacts_path
|
|
119
|
+
user_path(Current.user, tab: "contacts")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def render_ai_edit_failure
|
|
123
|
+
@available_tools = load_available_tools
|
|
124
|
+
@llm_models = Collavre::LlmModel.suggestions
|
|
125
|
+
@agent_gateways = editable_agent_gateways(@user)
|
|
126
|
+
@return_to = safe_return_to(params[:return_to])
|
|
127
|
+
flash.now[:alert] = @user.errors.full_messages.to_sentence
|
|
128
|
+
render :edit_ai, status: :unprocessable_entity
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Saving should land back on whatever list opened the form (the admin user
|
|
132
|
+
# list, a profile's contacts tab, the org chart), not a fixed page.
|
|
133
|
+
def update_ai_destination
|
|
134
|
+
safe_return_to(params[:return_to]) || current_user_contacts_path
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def safe_return_to(candidate)
|
|
138
|
+
path = local_path_for(candidate)
|
|
139
|
+
return nil if path.nil? || path.split("?").first == edit_ai_user_path(@user)
|
|
140
|
+
|
|
141
|
+
path
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def local_path_for(candidate)
|
|
145
|
+
return nil if candidate.blank?
|
|
146
|
+
|
|
147
|
+
uri = URI.parse(candidate)
|
|
148
|
+
return nil if uri.host.present? && uri.host != request.host
|
|
149
|
+
return nil unless uri.path.to_s.start_with?("/") && !uri.path.start_with?("//")
|
|
150
|
+
|
|
151
|
+
[ uri.path, uri.query ].compact_blank.join("?")
|
|
152
|
+
rescue URI::InvalidURIError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def remember_llm_model(user)
|
|
157
|
+
Collavre::LlmModel.remember!(
|
|
158
|
+
vendor: user.llm_vendor,
|
|
159
|
+
name: user.llm_model,
|
|
160
|
+
creator: Current.user
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
74
164
|
def load_available_tools
|
|
75
165
|
Collavre::McpService.available_tools(Current.user).map do |tool|
|
|
76
166
|
{
|
|
@@ -81,6 +171,26 @@ module Collavre
|
|
|
81
171
|
end
|
|
82
172
|
end
|
|
83
173
|
|
|
174
|
+
def selected_agent_gateway
|
|
175
|
+
return unless params[:llm_vendor] == "cli_proxy"
|
|
176
|
+
|
|
177
|
+
Current.user.owned_agent_gateways.active.find_by(id: params[:agent_gateway_id])
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def gateway_owner_for(agent)
|
|
181
|
+
agent.creator || Current.user
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def editable_agent_gateways(agent)
|
|
185
|
+
gateways = gateway_owner_for(agent).owned_agent_gateways
|
|
186
|
+
selected_gateway = gateways.find_by(id: agent.agent_gateway_id)
|
|
187
|
+
(chat_capable_agent_gateways(gateway_owner_for(agent)) + [ selected_gateway ]).compact.uniq.sort_by(&:name)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def chat_capable_agent_gateways(owner)
|
|
191
|
+
owner.owned_agent_gateways.active.order(:name).select(&:chat_capable?)
|
|
192
|
+
end
|
|
193
|
+
|
|
84
194
|
def set_user_for_ai_actions
|
|
85
195
|
@user = Collavre::User.find(params[:id])
|
|
86
196
|
end
|
|
@@ -58,14 +58,6 @@ module Collavre
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
def typo_correction
|
|
62
|
-
@user = Collavre::User.find(params[:id])
|
|
63
|
-
|
|
64
|
-
unless @user == Current.user || Current.user.system_admin?
|
|
65
|
-
redirect_to user_path(Current.user), alert: I18n.t("collavre.users.destroy.not_authorized")
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
61
|
def update_password
|
|
70
62
|
@user = Collavre::User.find(params[:id])
|
|
71
63
|
if @user.authenticate(params[:user][:current_password])
|
|
@@ -98,13 +90,7 @@ module Collavre
|
|
|
98
90
|
:calendar_id,
|
|
99
91
|
:timezone,
|
|
100
92
|
:locale,
|
|
101
|
-
:
|
|
102
|
-
:typo_correction_threshold,
|
|
103
|
-
:typo_correction_on_soft_keyboard,
|
|
104
|
-
:typo_correction_on_voice,
|
|
105
|
-
:typo_correction_on_physical_keyboard,
|
|
106
|
-
:typo_correction_in_chat,
|
|
107
|
-
:typo_correction_in_editor
|
|
93
|
+
:creative_workspace_enabled
|
|
108
94
|
).tap do |p|
|
|
109
95
|
p[:locale] = normalize_supported_locale(p[:locale]) if p.key?(:locale)
|
|
110
96
|
end
|
|
@@ -11,7 +11,7 @@ module Collavre
|
|
|
11
11
|
@user = Collavre::User.new
|
|
12
12
|
if params[:invite_token].present?
|
|
13
13
|
@invitation = Collavre::Invitation.find_by_token_for(:invite, params[:invite_token])
|
|
14
|
-
@user.email = @invitation&.email
|
|
14
|
+
@user.email = @invitation.email if @invitation&.email.present?
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -22,7 +22,7 @@ module Collavre
|
|
|
22
22
|
invitation = Collavre::Invitation.find_by_token_for(:invite, params[:invite_token])
|
|
23
23
|
if invitation
|
|
24
24
|
@invitation = invitation
|
|
25
|
-
@user.email = invitation.email
|
|
25
|
+
@user.email = invitation.email if invitation.email.present?
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
if @user.save
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
# Raised by AgentLifecycleManager when an agent turn exceeds its wall-clock
|
|
5
|
+
# deadline (SystemSetting.ai_agent_turn_deadline_seconds).
|
|
6
|
+
#
|
|
7
|
+
# Subclasses CancelledError deliberately: AiClient#chat re-raises
|
|
8
|
+
# CancelledError but swallows every other StandardError into a streamed
|
|
9
|
+
# "AI Error" message, and the CancelledError rescue chain (AiAgentService,
|
|
10
|
+
# AiAgentJob) already preserves partial content, keeps the row's terminal
|
|
11
|
+
# status, releases the resource slot and drains the topic queue.
|
|
12
|
+
class TurnDeadlineError < CancelledError
|
|
13
|
+
attr_reader :deadline_seconds
|
|
14
|
+
|
|
15
|
+
def initialize(deadline_seconds)
|
|
16
|
+
@deadline_seconds = deadline_seconds
|
|
17
|
+
super("Turn exceeded the #{deadline_seconds}s deadline")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -6,6 +6,7 @@ module Collavre
|
|
|
6
6
|
COLLAVRE_STYLESHEETS = %w[
|
|
7
7
|
collavre/design_tokens
|
|
8
8
|
collavre/dark_mode
|
|
9
|
+
collavre/secret_fields
|
|
9
10
|
collavre/gnb
|
|
10
11
|
collavre/creatives
|
|
11
12
|
collavre/actiontext
|
|
@@ -14,6 +15,7 @@ module Collavre
|
|
|
14
15
|
collavre/org_chart
|
|
15
16
|
collavre/popup
|
|
16
17
|
collavre/comments_popup
|
|
18
|
+
collavre/workspace
|
|
17
19
|
collavre/tables
|
|
18
20
|
collavre/code_highlight
|
|
19
21
|
collavre/comment_versions
|
|
@@ -43,6 +45,14 @@ module Collavre
|
|
|
43
45
|
safe_join(tags, "\n ")
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
def masked_secret_field(form, method, options = {})
|
|
49
|
+
form.text_field(method, masked_secret_field_options(options))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def masked_secret_field_tag(name, value = nil, options = {})
|
|
53
|
+
text_field_tag(name, value, masked_secret_field_options(options))
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
# Returns the body CSS class for the current user's theme.
|
|
47
57
|
# Custom themes get "light-mode" to disable OS dark mode overrides,
|
|
48
58
|
# ensuring the custom theme controls all colors regardless of OS setting.
|
|
@@ -60,6 +70,25 @@ module Collavre
|
|
|
60
70
|
end
|
|
61
71
|
end
|
|
62
72
|
|
|
73
|
+
# The creative index is reachable at both "/" (root route) and
|
|
74
|
+
# "/creatives", so the list filters cannot decide "am I already on the
|
|
75
|
+
# index?" from the pathname alone — they read this instead.
|
|
76
|
+
def creative_index_page?
|
|
77
|
+
controller_path == "collavre/creatives" && action_name == "index"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def creative_workspace?
|
|
81
|
+
Current.user&.creative_workspace_enabled? &&
|
|
82
|
+
creative_index_page? &&
|
|
83
|
+
!@auto_fullscreen
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Identifies requests made by the packaged Tauri shell. Desktop-only UI
|
|
87
|
+
# fallbacks should use this rather than attempting platform browser checks.
|
|
88
|
+
def desktop?
|
|
89
|
+
request.user_agent.to_s.start_with?("CollavreDesktop/")
|
|
90
|
+
end
|
|
91
|
+
|
|
63
92
|
# Renders CSS for admin-configured default themes (light/dark mode)
|
|
64
93
|
# Only applies when the user has no personal theme set
|
|
65
94
|
def default_theme_styles
|
|
@@ -97,6 +126,15 @@ module Collavre
|
|
|
97
126
|
|
|
98
127
|
private
|
|
99
128
|
|
|
129
|
+
def masked_secret_field_options(options)
|
|
130
|
+
options.merge(
|
|
131
|
+
autocomplete: "off",
|
|
132
|
+
autocapitalize: "none",
|
|
133
|
+
spellcheck: false,
|
|
134
|
+
class: class_names("masked-secret-field", options[:class])
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
100
138
|
CSS_VARIABLE_KEY_PATTERN = /\A--[a-zA-Z0-9_-]+\z/
|
|
101
139
|
CSS_VARIABLE_VALUE_PATTERN = /\A[^;}{<>"']+\z/
|
|
102
140
|
ALLOWED_COLOR_SCHEMES = %w[light dark].freeze
|