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,5 +1,6 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CommentsController < ApplicationController
|
|
3
|
+
LEGACY_TOPIC_WATERMARK_KEY = "_legacy"
|
|
3
4
|
include Collavre::Comments::CommentScoping
|
|
4
5
|
include Collavre::Comments::ApprovalActions
|
|
5
6
|
include Collavre::Comments::Conversion
|
|
@@ -25,134 +26,7 @@ module Collavre
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def index
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
visible_scope = @creative.comments.visible_to(Current.user)
|
|
31
|
-
scope = visible_scope.with_attached_images.includes(:task, :topic, :comment_reactions, :comment_versions, :snapshot_as_result)
|
|
32
|
-
|
|
33
|
-
if params[:search].present?
|
|
34
|
-
words = params[:search].to_s.strip.downcase.split(/\s+/)
|
|
35
|
-
.first(Creatives::Filters::SearchFilter::MAX_SEARCH_WORDS)
|
|
36
|
-
words.each do |word|
|
|
37
|
-
sanitized = "%#{ActiveRecord::Base.sanitize_sql_like(word)}%"
|
|
38
|
-
scope = scope.where("LOWER(comments.content) LIKE ?", sanitized)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Filter by topic
|
|
43
|
-
# Logic:
|
|
44
|
-
# 1. Prefer params[:topic_id] if explicit.
|
|
45
|
-
# 2. If deep linking (around_comment_id), infer from target comment if valid.
|
|
46
|
-
# 3. Default to nil (Main).
|
|
47
|
-
|
|
48
|
-
effective_topic_id = params[:topic_id].presence
|
|
49
|
-
|
|
50
|
-
if params[:around_comment_id].present?
|
|
51
|
-
target_id = params[:around_comment_id].to_i
|
|
52
|
-
# Ensure target is visible and belongs to this creative
|
|
53
|
-
target_comment = visible_scope.find_by(id: target_id)
|
|
54
|
-
|
|
55
|
-
if target_comment
|
|
56
|
-
effective_topic_id = target_comment.topic_id
|
|
57
|
-
# Inform frontend about the topic switch
|
|
58
|
-
response.headers["X-Topic-Id"] = effective_topic_id.to_s
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Apply the Topic Filter
|
|
63
|
-
if effective_topic_id.present?
|
|
64
|
-
scope = scope.where(topic_id: effective_topic_id)
|
|
65
|
-
else
|
|
66
|
-
# Main view: exclude comments from archived topics
|
|
67
|
-
archived_topic_ids = @creative.topics.archived.pluck(:id)
|
|
68
|
-
scope = scope.where.not(topic_id: archived_topic_ids) if archived_topic_ids.any?
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Default order: Newest first (created_at DESC)
|
|
72
|
-
# This matches the column-reverse layout where the first item in the list is the visual bottom (Newest).
|
|
73
|
-
scope = scope.order(created_at: :desc)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@comments = if params[:around_comment_id].present?
|
|
77
|
-
# Deep linking: Load context around a specific comment
|
|
78
|
-
target_id = params[:around_comment_id].to_i
|
|
79
|
-
|
|
80
|
-
# Newer messages have HIGHER IDs.
|
|
81
|
-
# Older messages have LOWER IDs.
|
|
82
|
-
|
|
83
|
-
# Newer bundle (including target): ID >= target_id
|
|
84
|
-
newer_bundle = scope.where("comments.id >= ?", target_id).reorder(created_at: :asc).limit(limit / 2 + 1)
|
|
85
|
-
|
|
86
|
-
# Older bundle: ID < target_id
|
|
87
|
-
older_bundle = scope.where("comments.id < ?", target_id).limit(limit / 2)
|
|
88
|
-
|
|
89
|
-
# Combine: [Newer (ASC) ... Target ... Older (DESC)]
|
|
90
|
-
# We need final output to be ASC due to restored view logic: [Oldest ... Target ... Newest]
|
|
91
|
-
(older_bundle.to_a.reverse + newer_bundle.to_a).uniq
|
|
92
|
-
elsif params[:after_id].present? && params[:before_id].present?
|
|
93
|
-
# Invalid state, prioritize before (loading older history)
|
|
94
|
-
scope.where("comments.id < ?", params[:before_id].to_i).limit(limit).to_a.reverse
|
|
95
|
-
elsif params[:before_id].present?
|
|
96
|
-
# Load OLDER messages (lower IDs)
|
|
97
|
-
# Visually scrolling UP
|
|
98
|
-
scope.where("comments.id < ?", params[:before_id].to_i).limit(limit).to_a.reverse
|
|
99
|
-
elsif params[:after_id].present?
|
|
100
|
-
# Load NEWER messages (higher IDs)
|
|
101
|
-
# Visually scrolling DOWN
|
|
102
|
-
# We want the ones immediately *after* the current newest.
|
|
103
|
-
# Since default sort is DESC (Newest first), "after" means id > after_id.
|
|
104
|
-
# But standard DESC query would give us the VERY Newest.
|
|
105
|
-
# We want the ones just above `after_id`.
|
|
106
|
-
|
|
107
|
-
# Use reorder(ASC) to get the ones immediately larger than after_id, then reverse back to DESC.
|
|
108
|
-
scope.where("comments.id > ?", params[:after_id].to_i).reorder(created_at: :asc).limit(limit)
|
|
109
|
-
else
|
|
110
|
-
# Initial Load (Latest messages)
|
|
111
|
-
scope.limit(limit).to_a.reverse
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
present_user_ids = CommentPresenceStore.list(@creative.id)
|
|
115
|
-
|
|
116
|
-
read_receipts = {}
|
|
117
|
-
if @comments.any?
|
|
118
|
-
# Fetch all read pointers for this creative that point to comments in the current list
|
|
119
|
-
# We only care about pointers that match the IDs of the comments we are displaying?
|
|
120
|
-
# Or rather, we want to show the 'line' on the comment that matches the pointer.
|
|
121
|
-
|
|
122
|
-
# Optimization: Fetch all pointers for participants of this creative.
|
|
123
|
-
# Scoped to the creative.
|
|
124
|
-
pointers = CommentReadPointer.where(creative: @creative)
|
|
125
|
-
.where.not(last_read_comment_id: nil)
|
|
126
|
-
.includes(user: { avatar_attachment: :blob })
|
|
127
|
-
|
|
128
|
-
# Fetch all visible IDs for correct read-receipt placement transparency
|
|
129
|
-
# Only map read receipts to PUBLIC comments.
|
|
130
|
-
# Users who read private comments will appear on the nearest preceding public comment.
|
|
131
|
-
public_ids = @creative.comments.public_only.order(id: :asc).pluck(:id)
|
|
132
|
-
|
|
133
|
-
pointers.each do |pointer|
|
|
134
|
-
effective_id = pointer.effective_comment_id(public_ids)
|
|
135
|
-
if effective_id
|
|
136
|
-
read_receipts[effective_id] ||= []
|
|
137
|
-
read_receipts[effective_id] << pointer.user
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
if params[:after_id].present? || params[:before_id].present?
|
|
143
|
-
render partial: "collavre/comments/comment",
|
|
144
|
-
collection: @comments,
|
|
145
|
-
as: :comment,
|
|
146
|
-
locals: { read_receipts: read_receipts, present_user_ids: present_user_ids, current_topic_id: effective_topic_id }
|
|
147
|
-
else
|
|
148
|
-
render partial: "collavre/comments/list", locals: {
|
|
149
|
-
comments: @comments,
|
|
150
|
-
creative: @creative,
|
|
151
|
-
read_receipts: read_receipts,
|
|
152
|
-
present_user_ids: present_user_ids,
|
|
153
|
-
current_topic_id: effective_topic_id
|
|
154
|
-
}
|
|
155
|
-
end
|
|
29
|
+
Collavre::Comments::ListResponse.new(controller: self, creative: @creative).render
|
|
156
30
|
end
|
|
157
31
|
|
|
158
32
|
def create
|
|
@@ -173,7 +47,9 @@ module Collavre
|
|
|
173
47
|
|
|
174
48
|
@comment.user = Current.user
|
|
175
49
|
@comment.images.attach(image_attachments) if image_attachments.present?
|
|
176
|
-
response =
|
|
50
|
+
response = unless inbox_system_comment?
|
|
51
|
+
::Comments::CommandProcessor.new(comment: @comment, user: Current.user).call
|
|
52
|
+
end
|
|
177
53
|
if response.present?
|
|
178
54
|
@comment.content = "#{@comment.content}\n\n#{response}"
|
|
179
55
|
@comment.skip_dispatch = true
|
|
@@ -199,7 +75,7 @@ module Collavre
|
|
|
199
75
|
safe_params = comment_params.except(:quoted_comment_id, :quoted_text)
|
|
200
76
|
validate_topic_id!(safe_params[:topic_id]) or return
|
|
201
77
|
|
|
202
|
-
if
|
|
78
|
+
if update_comment(safe_params)
|
|
203
79
|
@comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
|
|
204
80
|
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
|
|
205
81
|
else
|
|
@@ -225,7 +101,7 @@ module Collavre
|
|
|
225
101
|
if (is_admin || is_creative_owner) && !is_owner && @comment.user.present? && !@comment.user.ai_user?
|
|
226
102
|
inbox_creative = Creative.inbox_for(@comment.user)
|
|
227
103
|
creative_path = Collavre::Engine.routes.url_helpers.creative_path(@creative, open_comments: true)
|
|
228
|
-
creative_link = "[#{@creative.
|
|
104
|
+
creative_link = "[#{@creative.creative_snippet_markdown}](#{creative_path})"
|
|
229
105
|
msg = I18n.t(
|
|
230
106
|
"inbox.comment_deleted_by_admin",
|
|
231
107
|
admin_name: Current.user.name,
|
|
@@ -352,10 +228,48 @@ module Collavre
|
|
|
352
228
|
params[:topic_id].presence || params.dig(:comment, :topic_id).presence
|
|
353
229
|
end
|
|
354
230
|
|
|
231
|
+
def inbox_system_comment?
|
|
232
|
+
@creative.inbox? && @comment.topic&.name == Creative::SYSTEM_TOPIC_NAME
|
|
233
|
+
end
|
|
234
|
+
|
|
355
235
|
def validate_topic_id!(topic_id)
|
|
356
236
|
return true if topic_id.blank? || @creative.topics.where(id: topic_id).exists?
|
|
357
237
|
render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity
|
|
358
238
|
false
|
|
359
239
|
end
|
|
240
|
+
|
|
241
|
+
def update_comment(attributes)
|
|
242
|
+
return update_comment_under_topic_lock(attributes) unless attributes.key?(:topic_id)
|
|
243
|
+
|
|
244
|
+
attributes = attributes.dup
|
|
245
|
+
topic_id = attributes.delete(:topic_id)
|
|
246
|
+
Comment.transaction do
|
|
247
|
+
CommentMoveService.new(creative: @creative, user: Current.user).call(
|
|
248
|
+
comment_ids: [ @comment.id ], target_topic_id: topic_id
|
|
249
|
+
)
|
|
250
|
+
updated = @comment.reload.update(attributes)
|
|
251
|
+
raise ActiveRecord::Rollback unless updated
|
|
252
|
+
|
|
253
|
+
updated
|
|
254
|
+
end
|
|
255
|
+
rescue CommentMoveService::MoveError => e
|
|
256
|
+
@comment.errors.add(:topic, e.message)
|
|
257
|
+
false
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def update_comment_under_topic_lock(attributes)
|
|
261
|
+
source_topic_id = @comment.topic_id
|
|
262
|
+
matching_context = false
|
|
263
|
+
updated = false
|
|
264
|
+
Comments::TopicMutation.call(source_topic_id, @creative.id) do
|
|
265
|
+
@comment.lock!
|
|
266
|
+
matching_context = @comment.creative_id == @creative.id && @comment.topic_id == source_topic_id
|
|
267
|
+
updated = @comment.update(attributes) if matching_context
|
|
268
|
+
end
|
|
269
|
+
return updated if matching_context
|
|
270
|
+
|
|
271
|
+
@comment.errors.add(:topic, I18n.t("collavre.comments.move_not_allowed"))
|
|
272
|
+
false
|
|
273
|
+
end
|
|
360
274
|
end
|
|
361
275
|
end
|
|
@@ -5,18 +5,24 @@ module Collavre
|
|
|
5
5
|
|
|
6
6
|
def request_permission
|
|
7
7
|
creative = @creative.effective_origin
|
|
8
|
-
if creative.user == Current.user || creative.has_permission?(Current.user, :
|
|
8
|
+
if creative.user == Current.user || creative.has_permission?(Current.user, :write)
|
|
9
9
|
return head :unprocessable_entity
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# A requester who already has some access (read or feedback) is
|
|
13
|
+
# specifically asking for a write upgrade — distinguish that from a
|
|
14
|
+
# from-zero access request so the owner knows which one it is.
|
|
15
|
+
already_has_read_access = creative.has_permission?(Current.user, :read)
|
|
16
|
+
|
|
17
|
+
short_title = Collavre::HtmlText.markdown_label(creative.effective_origin.description, 10)
|
|
13
18
|
creative_path = Collavre::Engine.routes.url_helpers.creative_path(creative, open_comments: true)
|
|
14
19
|
creative_link = "[#{short_title}](#{creative_path})"
|
|
15
20
|
|
|
16
21
|
inbox_creative = Creative.inbox_for(creative.user)
|
|
17
22
|
system_topic = inbox_creative.system_topic(fallback_user: creative.user)
|
|
23
|
+
i18n_key = already_has_read_access ? "inbox.write_permission_requested" : "inbox.permission_requested"
|
|
18
24
|
msg = I18n.t(
|
|
19
|
-
|
|
25
|
+
i18n_key,
|
|
20
26
|
user: Current.user.display_name,
|
|
21
27
|
short_title: creative_link,
|
|
22
28
|
locale: creative.user.locale || "en"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class CreativesController < ApplicationController
|
|
3
|
+
WORKSPACE_TREE_EXPANSION_LIMIT = 100
|
|
4
|
+
|
|
3
5
|
include Collavre::Concerns::SlideViewable
|
|
4
6
|
include Collavre::Concerns::Exportable
|
|
5
7
|
include Collavre::Concerns::TreeManageable
|
|
@@ -17,20 +19,40 @@ module Collavre
|
|
|
17
19
|
# tracked separately and intentionally deferred.
|
|
18
20
|
allow_unauthenticated_access only: %i[ index children export_markdown show slide_view ]
|
|
19
21
|
before_action :enforce_creatives_login_policy, only: %i[ index children export_markdown show slide_view ]
|
|
20
|
-
before_action :set_creative, only: %i[ show edit update destroy
|
|
22
|
+
before_action :set_creative, only: %i[ show edit update destroy slide_view request_permission unconvert contexts update_contexts update_metadata archive unarchive trigger_action remember_last_visited ]
|
|
21
23
|
before_action :require_creative_write!, only: %i[archive unarchive]
|
|
22
24
|
|
|
23
25
|
def index
|
|
24
26
|
respond_to do |format|
|
|
25
27
|
format.html do
|
|
28
|
+
visit_received_at = Time.current
|
|
26
29
|
# HTML only needs parent_creative for nav/title - skip expensive filtered queries
|
|
27
30
|
# Must check permission to avoid leaking metadata (og:title, etc.) to unauthorized users
|
|
28
31
|
if params[:id].present?
|
|
29
32
|
creative = Creative.find_by(id: params[:id])
|
|
30
33
|
@parent_creative = creative if creative&.has_permission?(Current.user, :read)
|
|
34
|
+
if Current.user
|
|
35
|
+
@last_visited_creative_client_id = last_visited_creative_client_id
|
|
36
|
+
@last_visited_creative_visit_sequence = last_visited_creative_visit_sequence
|
|
37
|
+
unless request.head? || turbo_prefetch_request?
|
|
38
|
+
@last_visited_creative_visit_sequence = remember_last_visited_creative(
|
|
39
|
+
@parent_creative,
|
|
40
|
+
client_id: @last_visited_creative_client_id,
|
|
41
|
+
sequence: @last_visited_creative_visit_sequence,
|
|
42
|
+
received_at: visit_received_at
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
@last_visited_creative_token = last_visited_creative_token
|
|
46
|
+
end
|
|
31
47
|
end
|
|
32
48
|
@creatives = [] # CSR will fetch via JSON
|
|
33
49
|
@shared_list = @parent_creative ? @parent_creative.all_shared_users : []
|
|
50
|
+
if Current.user&.creative_workspace_enabled?
|
|
51
|
+
@workspace_path_ids = Creatives::WorkspacePathResolver.new(
|
|
52
|
+
creative: @parent_creative,
|
|
53
|
+
user: Current.user
|
|
54
|
+
).call
|
|
55
|
+
end
|
|
34
56
|
end
|
|
35
57
|
format.json do
|
|
36
58
|
# Full query only for JSON requests
|
|
@@ -55,6 +77,18 @@ module Collavre
|
|
|
55
77
|
@allowed_creative_ids = index_result.allowed_creative_ids
|
|
56
78
|
@progress_map = index_result.progress_map
|
|
57
79
|
|
|
80
|
+
if params[:workspace_tree] == "1"
|
|
81
|
+
expires_now
|
|
82
|
+
render json: {
|
|
83
|
+
creatives: Collavre::Creatives::WorkspaceTreeBuilder.new(
|
|
84
|
+
user: Current.user,
|
|
85
|
+
view_context: view_context,
|
|
86
|
+
expanded_ids: workspace_tree_expanded_ids
|
|
87
|
+
).build(index_result.creatives)
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
58
92
|
# Set filtered_progress on parent creative if progress_map is available
|
|
59
93
|
if @parent_creative && @progress_map && @progress_map.key?(@parent_creative.id.to_s)
|
|
60
94
|
@parent_creative.filtered_progress = @progress_map[@parent_creative.id.to_s]
|
|
@@ -232,15 +266,6 @@ module Collavre
|
|
|
232
266
|
end
|
|
233
267
|
end
|
|
234
268
|
|
|
235
|
-
def parent_suggestions
|
|
236
|
-
unless @creative.has_permission?(Current.user, :read)
|
|
237
|
-
render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
suggestions = ::GeminiParentRecommender.new.recommend(@creative)
|
|
241
|
-
render json: suggestions
|
|
242
|
-
end
|
|
243
|
-
|
|
244
269
|
def edit
|
|
245
270
|
unless @creative.has_permission?(Current.user, :write)
|
|
246
271
|
redirect_to @creative, alert: t("collavre.creatives.errors.no_permission") and return
|
|
@@ -526,6 +551,27 @@ module Collavre
|
|
|
526
551
|
head :ok
|
|
527
552
|
end
|
|
528
553
|
|
|
554
|
+
# Turbo may restore a cached workspace frame from browser history without
|
|
555
|
+
# issuing the HTML request that normally records this value. The client
|
|
556
|
+
# calls this endpoint after confirming the restored frame matches the URL.
|
|
557
|
+
def remember_last_visited
|
|
558
|
+
visit_received_at = Time.current
|
|
559
|
+
return head :forbidden unless @creative.has_permission?(Current.user, :read)
|
|
560
|
+
|
|
561
|
+
visit = restored_visit
|
|
562
|
+
return head :unprocessable_entity unless visit
|
|
563
|
+
|
|
564
|
+
remember_last_visited_creative(@creative, **visit, received_at: visit_received_at)
|
|
565
|
+
head :no_content
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Reserve an ordering value before Turbo starts a Creative navigation.
|
|
569
|
+
# Reserving does not change the remembered Creative, so a cancelled request
|
|
570
|
+
# leaves only a harmless gap in the sequence.
|
|
571
|
+
def next_last_visited_sequence
|
|
572
|
+
render json: { sequence: issue_last_visited_creative_sequence }
|
|
573
|
+
end
|
|
574
|
+
|
|
529
575
|
def destroy
|
|
530
576
|
parent = @creative.parent
|
|
531
577
|
unless @creative.has_permission?(Current.user, :admin)
|
|
@@ -539,6 +585,84 @@ module Collavre
|
|
|
539
585
|
end
|
|
540
586
|
|
|
541
587
|
private
|
|
588
|
+
def remember_last_visited_creative(creative, client_id:, sequence:, received_at:)
|
|
589
|
+
return unless Current.user && creative
|
|
590
|
+
|
|
591
|
+
Current.user.with_lock do
|
|
592
|
+
same_client = Current.user.last_visited_creative_client_id == client_id
|
|
593
|
+
sequence ||= issue_last_visited_creative_sequence_locked
|
|
594
|
+
return sequence if same_client && Current.user.last_visited_creative_visit_sequence.to_i >= sequence
|
|
595
|
+
return sequence if !same_client && Current.user.last_visited_creative_at &&
|
|
596
|
+
Current.user.last_visited_creative_at >= received_at
|
|
597
|
+
|
|
598
|
+
Current.user.update_columns(
|
|
599
|
+
last_visited_creative_id: creative.id,
|
|
600
|
+
# A request can wait on this lock after it reaches Rails. Retain
|
|
601
|
+
# its arrival time so an older request from another browser session
|
|
602
|
+
# cannot overwrite a Creative recorded by a later-arriving visit.
|
|
603
|
+
last_visited_creative_at: received_at,
|
|
604
|
+
last_visited_creative_client_id: client_id,
|
|
605
|
+
last_visited_creative_visit_sequence: sequence
|
|
606
|
+
)
|
|
607
|
+
sequence
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def issue_last_visited_creative_sequence
|
|
612
|
+
Current.user.with_lock { issue_last_visited_creative_sequence_locked }
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def issue_last_visited_creative_sequence_locked
|
|
616
|
+
sequence = [
|
|
617
|
+
Current.user.last_visited_creative_issued_sequence.to_i,
|
|
618
|
+
Current.user.last_visited_creative_visit_sequence.to_i
|
|
619
|
+
].max + 1
|
|
620
|
+
Current.user.update_column(:last_visited_creative_issued_sequence, sequence)
|
|
621
|
+
sequence
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def last_visited_creative_client_id
|
|
625
|
+
session[:last_visited_creative_client_id] ||= SecureRandom.uuid
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def last_visited_creative_visit_sequence
|
|
629
|
+
supplied_sequence = request.headers["X-Collavre-Last-Visited-Creative-Sequence"].to_i
|
|
630
|
+
supplied_sequence if supplied_sequence.positive? && visit_token_client_id == last_visited_creative_client_id
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def last_visited_creative_token
|
|
634
|
+
return unless @parent_creative && @last_visited_creative_client_id && @last_visited_creative_visit_sequence
|
|
635
|
+
|
|
636
|
+
Rails.application.message_verifier(:last_visited_creative).generate({
|
|
637
|
+
"creative_id" => @parent_creative.id,
|
|
638
|
+
"client_id" => @last_visited_creative_client_id,
|
|
639
|
+
"sequence" => @last_visited_creative_visit_sequence
|
|
640
|
+
})
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def restored_visit
|
|
644
|
+
visit = Rails.application.message_verifier(:last_visited_creative).verified(params[:visit_token])
|
|
645
|
+
sequence = request.headers["X-Collavre-Last-Visited-Creative-Sequence"].to_i
|
|
646
|
+
return unless visit.is_a?(Hash) && visit["creative_id"] == @creative.id &&
|
|
647
|
+
visit["client_id"] == last_visited_creative_client_id
|
|
648
|
+
|
|
649
|
+
{ client_id: visit.fetch("client_id"), sequence: sequence.positive? ? sequence : nil }
|
|
650
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature, KeyError, ArgumentError
|
|
651
|
+
nil
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def visit_token_client_id
|
|
655
|
+
token = request.headers["X-Collavre-Last-Visited-Creative-Token"]
|
|
656
|
+
visit = Rails.application.message_verifier(:last_visited_creative).verified(token)
|
|
657
|
+
visit["client_id"] if visit.is_a?(Hash)
|
|
658
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
659
|
+
nil
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def turbo_prefetch_request?
|
|
663
|
+
request.headers["X-Sec-Purpose"] == "prefetch"
|
|
664
|
+
end
|
|
665
|
+
|
|
542
666
|
def build_tree(collection, params:, expanded_state_map:, level:, select_mode: false, allowed_creative_ids: nil, progress_map: nil)
|
|
543
667
|
::Creatives::TreeBuilder.new(
|
|
544
668
|
user: Current.user,
|
|
@@ -577,6 +701,15 @@ module Collavre
|
|
|
577
701
|
).to_h
|
|
578
702
|
end
|
|
579
703
|
|
|
704
|
+
def workspace_tree_expanded_ids
|
|
705
|
+
permitted = params.permit(expand: [])[:expand]
|
|
706
|
+
Array(permitted).filter_map { |id| Integer(id, exception: false) }
|
|
707
|
+
.uniq
|
|
708
|
+
.first(WORKSPACE_TREE_EXPANSION_LIMIT)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
helper_method :any_filter_active?
|
|
712
|
+
|
|
580
713
|
def any_filter_active?
|
|
581
714
|
params[:tags].present? ||
|
|
582
715
|
params[:min_progress].present? ||
|
|
@@ -677,7 +810,7 @@ module Collavre
|
|
|
677
810
|
skip_dispatch: true
|
|
678
811
|
)
|
|
679
812
|
|
|
680
|
-
scheduled = SystemEvents::Dispatcher.dispatch("comment_created", comment.dispatch_payload)
|
|
813
|
+
scheduled = SystemEvents::Dispatcher.dispatch("comment_created", comment.dispatch_payload, source: "trigger_restart")
|
|
681
814
|
Rails.logger.info("[TriggerAction] restart: posted trigger comment #{comment.id}, dispatched to #{scheduled&.size || 0} agents")
|
|
682
815
|
end
|
|
683
816
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Collavre
|
|
6
|
+
# Stateless, loopback-only native handoff for the desktop shell. It has no
|
|
7
|
+
# browser session or CSRF cookie; authorization is the signed, short-lived
|
|
8
|
+
# grant issued to the authenticated desktop webview.
|
|
9
|
+
class DesktopGatewayRegistrationsController < ActionController::API
|
|
10
|
+
# Native code calls this before it creates Keychain credentials or starts
|
|
11
|
+
# the local proxy. The signed token is the administrator's short-lived
|
|
12
|
+
# setup consent and remains the sole authorization for the sessionless
|
|
13
|
+
# loopback handoff.
|
|
14
|
+
def validate_registration_grant
|
|
15
|
+
return unless require_desktop_mode!
|
|
16
|
+
return unless require_loopback!
|
|
17
|
+
|
|
18
|
+
registration_owner!
|
|
19
|
+
head :no_content
|
|
20
|
+
rescue ActionController::ParameterMissing, ActiveSupport::MessageVerifier::InvalidSignature => e
|
|
21
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create
|
|
25
|
+
return unless require_desktop_mode!
|
|
26
|
+
return unless require_loopback!
|
|
27
|
+
|
|
28
|
+
registration_owner = registration_owner!
|
|
29
|
+
# A desktop has one local proxy, not one proxy per administrator. The
|
|
30
|
+
# persisted desktop_managed marker identifies it independently of its
|
|
31
|
+
# user-editable display name and owner during recovery.
|
|
32
|
+
gateway = Collavre::AgentGateway.find_or_initialize_by(desktop_managed: true)
|
|
33
|
+
gateway.owner ||= registration_owner
|
|
34
|
+
gateway.name ||= available_desktop_gateway_name(gateway.owner)
|
|
35
|
+
owner = gateway.owner
|
|
36
|
+
gateway_attributes = {
|
|
37
|
+
base_url: "http://127.0.0.1:#{proxy_port}",
|
|
38
|
+
admin_key: params.require(:admin_key),
|
|
39
|
+
completion_key: params.require(:completion_key),
|
|
40
|
+
identity_secret: params.require(:identity_secret),
|
|
41
|
+
tenant_id: "collavre-desktop",
|
|
42
|
+
workspace_mode: :shared,
|
|
43
|
+
active: true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Collavre::AgentGateway.transaction do
|
|
47
|
+
gateway.update_from_desktop_registration!(gateway_attributes)
|
|
48
|
+
detected_adapters.each { |adapter| provision_preset!(owner, gateway, adapter) }
|
|
49
|
+
end
|
|
50
|
+
render json: { gateway_id: gateway.id, adapters: detected_adapters }, status: :created
|
|
51
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
52
|
+
render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity
|
|
53
|
+
rescue ActiveRecord::RecordNotUnique
|
|
54
|
+
# Concurrent first-run handoffs can both observe no marked gateway. Once
|
|
55
|
+
# one creates it, reload that durable identity and apply this handoff.
|
|
56
|
+
raise if @retried_after_gateway_insert
|
|
57
|
+
|
|
58
|
+
@retried_after_gateway_insert = true
|
|
59
|
+
retry
|
|
60
|
+
rescue ActionController::ParameterMissing, ActiveSupport::MessageVerifier::InvalidSignature => e
|
|
61
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Native startup checks this before trusting its persisted `registered`
|
|
65
|
+
# flag. It proves that the Rails gateway still exists at this proxy port
|
|
66
|
+
# and still holds the Keychain credentials without exposing them to the
|
|
67
|
+
# webview or requiring a browser session.
|
|
68
|
+
def registered
|
|
69
|
+
return unless require_desktop_mode!
|
|
70
|
+
return unless require_loopback!
|
|
71
|
+
|
|
72
|
+
gateway = Collavre::AgentGateway.find_by(
|
|
73
|
+
desktop_managed: true,
|
|
74
|
+
base_url: "http://127.0.0.1:#{proxy_port}"
|
|
75
|
+
)
|
|
76
|
+
return head :not_found unless gateway && gateway_credentials_match?(gateway)
|
|
77
|
+
|
|
78
|
+
head :no_content
|
|
79
|
+
rescue ActionController::ParameterMissing => e
|
|
80
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def proxy_port
|
|
86
|
+
Integer(params.require(:proxy_port).to_s, 10).tap do |port|
|
|
87
|
+
raise ActionController::ParameterMissing, :proxy_port unless (1..65_535).cover?(port)
|
|
88
|
+
end
|
|
89
|
+
rescue ArgumentError
|
|
90
|
+
raise ActionController::ParameterMissing, :proxy_port
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def detected_adapters
|
|
94
|
+
Array(params[:adapters]).map(&:to_s).intersection(DesktopSetupController::PRESETS.keys)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def gateway_credentials_match?(gateway)
|
|
98
|
+
%i[admin_key completion_key identity_secret].all? do |attribute|
|
|
99
|
+
ActiveSupport::SecurityUtils.secure_compare(gateway.public_send(attribute), params.require(attribute))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def provision_preset!(owner, gateway, adapter)
|
|
104
|
+
name, email, model = DesktopSetupController::PRESETS.fetch(adapter)
|
|
105
|
+
agent = owner.created_ai_users.find_or_initialize_by(desktop_preset_adapter: adapter)
|
|
106
|
+
if agent.new_record?
|
|
107
|
+
agent.assign_attributes(
|
|
108
|
+
name: name,
|
|
109
|
+
password: SecureRandom.hex(36),
|
|
110
|
+
email: available_desktop_preset_email(email, gateway),
|
|
111
|
+
email_verified_at: Time.current,
|
|
112
|
+
llm_vendor: "cli_proxy",
|
|
113
|
+
llm_model: model,
|
|
114
|
+
agent_gateway: gateway,
|
|
115
|
+
searchable: false,
|
|
116
|
+
tools: []
|
|
117
|
+
)
|
|
118
|
+
agent.save!
|
|
119
|
+
end
|
|
120
|
+
Collavre::Contact.ensure(user: owner, contact_user: agent)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def available_desktop_preset_email(email, gateway)
|
|
124
|
+
return email unless Collavre::User.exists?(email: email)
|
|
125
|
+
|
|
126
|
+
local_part, domain = email.split("@", 2)
|
|
127
|
+
base = "#{local_part}+desktop-#{gateway.id}"
|
|
128
|
+
candidate = "#{base}@#{domain}"
|
|
129
|
+
return candidate unless Collavre::User.exists?(email: candidate)
|
|
130
|
+
|
|
131
|
+
suffix = (2..).find do |number|
|
|
132
|
+
!Collavre::User.exists?(email: "#{base}-#{number}@#{domain}")
|
|
133
|
+
end
|
|
134
|
+
"#{base}-#{suffix}@#{domain}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def available_desktop_gateway_name(owner)
|
|
138
|
+
base_name = DesktopSetupController::DESKTOP_GATEWAY_NAME
|
|
139
|
+
return base_name unless owner.owned_agent_gateways.exists?(name: base_name)
|
|
140
|
+
|
|
141
|
+
suffix = (2..).find { |number| !owner.owned_agent_gateways.exists?(name: "#{base_name} (#{number})") }
|
|
142
|
+
"#{base_name} (#{suffix})"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def registration_owner!
|
|
146
|
+
payload = Rails.application.message_verifier("desktop-gateway-registration").verify(params.require(:registration_token))
|
|
147
|
+
raise ActiveSupport::MessageVerifier::InvalidSignature if payload.fetch("expires_at").to_i < Time.current.to_i
|
|
148
|
+
|
|
149
|
+
Collavre::User.find(payload.fetch("user_id")).tap do |user|
|
|
150
|
+
raise ActiveSupport::MessageVerifier::InvalidSignature unless user.system_admin?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def require_loopback!
|
|
155
|
+
addr = IPAddr.new(request.remote_addr.to_s)
|
|
156
|
+
addr = addr.native if addr.respond_to?(:ipv4_mapped?) && addr.ipv4_mapped?
|
|
157
|
+
return true if addr.loopback?
|
|
158
|
+
|
|
159
|
+
head :forbidden
|
|
160
|
+
false
|
|
161
|
+
rescue IPAddr::InvalidAddressError
|
|
162
|
+
head :forbidden
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def require_desktop_mode!
|
|
167
|
+
return true if Rails.application.config.x.desktop_proxy_setup == true
|
|
168
|
+
|
|
169
|
+
head :not_found
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|