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
|
@@ -4,10 +4,16 @@ import { attachBundleDragImage } from '../../utils/drag_bundle_image'
|
|
|
4
4
|
import { renderMarkdownInContainer } from '../../lib/utils/markdown'
|
|
5
5
|
import creativesApi from '../../lib/api/creatives'
|
|
6
6
|
import { renderCreativeTree, dispatchCreativeTreeUpdated } from '../../creatives/tree_renderer'
|
|
7
|
-
import { updateCsrfTokenFromResponse } from '../../lib/api/csrf_fetch'
|
|
7
|
+
import csrfFetch, { updateCsrfTokenFromResponse } from '../../lib/api/csrf_fetch'
|
|
8
8
|
import { alertDialog, confirmDialog } from '../../lib/utils/dialog'
|
|
9
|
+
import PrevMessageNavigator from './prev_message_navigator'
|
|
9
10
|
// CommonPopup is now used via TopicSearchController (Stimulus)
|
|
10
11
|
|
|
12
|
+
// Gestures that mean the user moved the list themselves, invalidating the
|
|
13
|
+
// previous-message anchor. Only user input fires these — our own smooth scroll
|
|
14
|
+
// does not.
|
|
15
|
+
const PREV_MSG_USER_INPUT_EVENTS = ['wheel', 'touchstart', 'keydown', 'pointerdown']
|
|
16
|
+
|
|
11
17
|
export default class extends Controller {
|
|
12
18
|
static targets = ['list']
|
|
13
19
|
|
|
@@ -20,8 +26,13 @@ export default class extends Controller {
|
|
|
20
26
|
this.movingComments = false
|
|
21
27
|
this.manualSearchQuery = null
|
|
22
28
|
this.initialLoadComplete = false
|
|
29
|
+
this.highlightCreativeId = null
|
|
30
|
+
this._loadCommentsVersion = 0
|
|
31
|
+
this.markReadTimeout = null
|
|
32
|
+
this.prevMsgNavigator = new PrevMessageNavigator()
|
|
23
33
|
|
|
24
34
|
this.handleScroll = this.handleScroll.bind(this)
|
|
35
|
+
this.handlePrevMsgUserInput = this.handlePrevMsgUserInput.bind(this)
|
|
25
36
|
this.handleChange = this.handleChange.bind(this)
|
|
26
37
|
this.handleClick = this.handleClick.bind(this)
|
|
27
38
|
this.handleSubmit = this.handleSubmit.bind(this)
|
|
@@ -33,6 +44,9 @@ export default class extends Controller {
|
|
|
33
44
|
this.handleStreamRender = this.handleStreamRender.bind(this)
|
|
34
45
|
|
|
35
46
|
this.listTarget.addEventListener('scroll', this.handleScroll)
|
|
47
|
+
PREV_MSG_USER_INPUT_EVENTS.forEach((name) => {
|
|
48
|
+
this.listTarget.addEventListener(name, this.handlePrevMsgUserInput)
|
|
49
|
+
})
|
|
36
50
|
this.listTarget.addEventListener('change', this.handleChange)
|
|
37
51
|
this.listTarget.addEventListener('click', this.handleClick)
|
|
38
52
|
this.listTarget.addEventListener('submit', this.handleSubmit)
|
|
@@ -43,7 +57,7 @@ export default class extends Controller {
|
|
|
43
57
|
// If we have a creativeId from data attribute or parent (unlikely directly on list,
|
|
44
58
|
// usually set via onPopupOpened), try loading.
|
|
45
59
|
// If not, onPopupOpened will trigger it.
|
|
46
|
-
if (this.element.dataset.creativeId) {
|
|
60
|
+
if (this.element.dataset.creativeId && this.element.dataset.docked !== 'true') {
|
|
47
61
|
this.creativeId = this.element.dataset.creativeId
|
|
48
62
|
this.loadInitialComments()
|
|
49
63
|
}
|
|
@@ -62,19 +76,36 @@ export default class extends Controller {
|
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
handleTopicChange(event) {
|
|
79
|
+
const nextTopicId = event.detail.topicId
|
|
80
|
+
if (
|
|
81
|
+
this.currentTopicId !== undefined &&
|
|
82
|
+
String(this.currentTopicId || '') === String(nextTopicId || '')
|
|
83
|
+
) return
|
|
84
|
+
|
|
65
85
|
// During notifyChildControllers, topic loading fires change events before
|
|
66
86
|
// onPopupOpened sets up highlightAfterLoad. Suppress these to avoid a
|
|
67
87
|
// race where a non-highlight load overwrites the deep-link highlight load.
|
|
68
88
|
if (this.suppressTopicChangeLoad) {
|
|
69
|
-
this.
|
|
89
|
+
this.flushPendingRead()
|
|
90
|
+
this.currentTopicId = nextTopicId
|
|
70
91
|
return
|
|
71
92
|
}
|
|
72
|
-
this.
|
|
93
|
+
this.flushPendingRead()
|
|
94
|
+
this.currentTopicId = nextTopicId
|
|
95
|
+
// A user-selected topic supersedes any outstanding deep-link window.
|
|
96
|
+
// The old request is discarded by the topic/version guards below, while
|
|
97
|
+
// this replacement load starts from the selected topic's latest messages.
|
|
98
|
+
this.highlightAfterLoad = null
|
|
99
|
+
this.highlightCreativeId = null
|
|
73
100
|
this.resetToLatest()
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
disconnect() {
|
|
104
|
+
this.flushPendingRead({ keepalive: true })
|
|
77
105
|
this.listTarget.removeEventListener('scroll', this.handleScroll)
|
|
106
|
+
PREV_MSG_USER_INPUT_EVENTS.forEach((name) => {
|
|
107
|
+
this.listTarget.removeEventListener(name, this.handlePrevMsgUserInput)
|
|
108
|
+
})
|
|
78
109
|
this.listTarget.removeEventListener('change', this.handleChange)
|
|
79
110
|
this.listTarget.removeEventListener('click', this.handleClick)
|
|
80
111
|
this.listTarget.removeEventListener('submit', this.handleSubmit)
|
|
@@ -106,9 +137,19 @@ export default class extends Controller {
|
|
|
106
137
|
}
|
|
107
138
|
|
|
108
139
|
onPopupOpened({ creativeId, highlightId, topicId } = {}) {
|
|
140
|
+
this.flushPendingRead()
|
|
141
|
+
const normalizedCreativeId = String(creativeId || '')
|
|
142
|
+
const pendingHighlight = String(this.highlightCreativeId || '') === normalizedCreativeId
|
|
143
|
+
? this.highlightAfterLoad
|
|
144
|
+
: null
|
|
109
145
|
this.creativeId = creativeId
|
|
146
|
+
this.renderedAllTopicIds = null
|
|
147
|
+
this.renderedAllTopicWatermarks = null
|
|
148
|
+
this.renderedAllIncludesLegacy = false
|
|
149
|
+
this.initialLoadComplete = false
|
|
110
150
|
// highlightId from popup args takes precedence, else fallback to URL param if first load
|
|
111
|
-
this.highlightAfterLoad = highlightId || this.deepLinkCommentId
|
|
151
|
+
this.highlightAfterLoad = highlightId || pendingHighlight || this.deepLinkCommentId
|
|
152
|
+
this.highlightCreativeId = this.highlightAfterLoad ? normalizedCreativeId : null
|
|
112
153
|
|
|
113
154
|
if (topicId !== undefined) {
|
|
114
155
|
this.currentTopicId = topicId
|
|
@@ -124,6 +165,15 @@ export default class extends Controller {
|
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
onPopupClosed() {
|
|
168
|
+
this.flushPendingRead()
|
|
169
|
+
this._loadCommentsVersion += 1
|
|
170
|
+
this.creativeId = null
|
|
171
|
+
this.renderedAllTopicIds = null
|
|
172
|
+
this.renderedAllTopicWatermarks = null
|
|
173
|
+
this.renderedAllIncludesLegacy = false
|
|
174
|
+
this.highlightAfterLoad = null
|
|
175
|
+
this.highlightCreativeId = null
|
|
176
|
+
this.suppressTopicChangeLoad = false
|
|
127
177
|
this.resetState()
|
|
128
178
|
this.listTarget.innerHTML = ''
|
|
129
179
|
this.initialLoadComplete = false
|
|
@@ -152,6 +202,10 @@ export default class extends Controller {
|
|
|
152
202
|
if (!this.creativeId) return
|
|
153
203
|
if (this.selection.size > 0) return
|
|
154
204
|
|
|
205
|
+
// The list is about to be replaced wholesale; any anchor we hold is stale.
|
|
206
|
+
this.prevMsgNavigator.reset()
|
|
207
|
+
|
|
208
|
+
const requestVersion = ++this._loadCommentsVersion
|
|
155
209
|
const params = {}
|
|
156
210
|
if (this.highlightAfterLoad) {
|
|
157
211
|
params.around_comment_id = this.highlightAfterLoad
|
|
@@ -160,12 +214,19 @@ export default class extends Controller {
|
|
|
160
214
|
const requestTopicId = this.currentTopicId || ""
|
|
161
215
|
const requestCreativeId = this.creativeId
|
|
162
216
|
|
|
163
|
-
this.fetchComments(params).then((html) => {
|
|
217
|
+
this.fetchComments(params, { loadVersion: requestVersion }).then((html) => {
|
|
164
218
|
// Discard stale responses if creative or topic changed while fetching.
|
|
165
219
|
// This prevents a race condition where switching creatives causes
|
|
166
220
|
// the old creative's comments to overwrite the new creative's list.
|
|
221
|
+
if (requestVersion !== this._loadCommentsVersion) return
|
|
167
222
|
if (this.creativeId !== requestCreativeId) return
|
|
168
|
-
|
|
223
|
+
// A server-resolved deep link moves currentTopicId off the topic this
|
|
224
|
+
// request asked for. That is this request's own answer, not a stale one,
|
|
225
|
+
// so the topic guard has to let it through — otherwise the list sits on
|
|
226
|
+
// "Loading..." forever for every comment link pointing outside the
|
|
227
|
+
// restored topic.
|
|
228
|
+
if (!this.isServerResolvedTopic(requestVersion) &&
|
|
229
|
+
String(this.currentTopicId || "") !== String(requestTopicId)) return
|
|
169
230
|
|
|
170
231
|
this.listTarget.innerHTML = html
|
|
171
232
|
this.listTarget.dataset.currentTopicId = this.currentTopicId || ""
|
|
@@ -177,6 +238,7 @@ export default class extends Controller {
|
|
|
177
238
|
this.allNewerLoaded = false // We are likely in middle
|
|
178
239
|
this.highlightComment(this.highlightAfterLoad)
|
|
179
240
|
this.highlightAfterLoad = null
|
|
241
|
+
this.highlightCreativeId = null
|
|
180
242
|
} else {
|
|
181
243
|
// Standard load -> Scroll to bottom (latest)
|
|
182
244
|
this.scrollToBottom()
|
|
@@ -188,26 +250,41 @@ export default class extends Controller {
|
|
|
188
250
|
this.formController.focusTextarea()
|
|
189
251
|
}
|
|
190
252
|
this.element.dispatchEvent(new CustomEvent('comments--list:loaded', { bubbles: true }))
|
|
253
|
+
this.reportRenderedAllTopics()
|
|
191
254
|
this.markCommentsRead()
|
|
192
255
|
|
|
193
256
|
}).catch((error) => {
|
|
257
|
+
if (requestVersion !== this._loadCommentsVersion) return
|
|
258
|
+
if (this.creativeId !== requestCreativeId) return
|
|
259
|
+
if (!this.isServerResolvedTopic(requestVersion) &&
|
|
260
|
+
String(this.currentTopicId || "") !== String(requestTopicId)) return
|
|
194
261
|
this.listTarget.innerHTML = `<div class="comments-list-error">${error.message}</div>`
|
|
195
262
|
})
|
|
196
263
|
}
|
|
197
264
|
|
|
265
|
+
// fetchComments stamps the version of the request whose own response carried
|
|
266
|
+
// an X-Topic-Id that moved the selection. Only that request may ignore the
|
|
267
|
+
// stale-topic guard; every other load compares unequal and stays guarded.
|
|
268
|
+
isServerResolvedTopic(requestVersion) {
|
|
269
|
+
return this._serverTopicRequestVersion !== undefined &&
|
|
270
|
+
this._serverTopicRequestVersion === requestVersion
|
|
271
|
+
}
|
|
272
|
+
|
|
198
273
|
loadOlderComments() {
|
|
199
274
|
if (this.loadingOlder || this.allOlderLoaded || !this.creativeId) return
|
|
200
275
|
const minId = this.getMinId()
|
|
201
276
|
if (!minId) return
|
|
202
277
|
|
|
278
|
+
const requestContext = this.paginationRequestContext()
|
|
203
279
|
this.loadingOlder = true
|
|
204
280
|
|
|
205
281
|
// Standard Column: Older messages are at Top.
|
|
206
282
|
// We Prepend them.
|
|
207
283
|
const currentScrollHeight = this.listTarget.scrollHeight
|
|
208
284
|
|
|
209
|
-
this.fetchComments({ before_id: minId })
|
|
285
|
+
this.fetchComments({ before_id: minId }, { pagination: true })
|
|
210
286
|
.then((html) => {
|
|
287
|
+
if (!this.isCurrentPaginationContext(requestContext)) return
|
|
211
288
|
if (html.trim() === '') {
|
|
212
289
|
this.allOlderLoaded = true
|
|
213
290
|
return
|
|
@@ -215,6 +292,12 @@ export default class extends Controller {
|
|
|
215
292
|
// Prepend to start (Visual Top)
|
|
216
293
|
this.listTarget.insertAdjacentHTML('afterbegin', html)
|
|
217
294
|
renderMarkdownInContainer(this.listTarget)
|
|
295
|
+
const addedTopic = this.recordRenderedAllTopicWatermarks(
|
|
296
|
+
this.listTarget.querySelectorAll('.comment-item'),
|
|
297
|
+
{ includeNewTopics: true },
|
|
298
|
+
)
|
|
299
|
+
if (addedTopic) this.reportRenderedAllTopics()
|
|
300
|
+
this.markCommentsRead()
|
|
218
301
|
|
|
219
302
|
// Restore scroll position
|
|
220
303
|
const newScrollHeight = this.listTarget.scrollHeight
|
|
@@ -222,7 +305,7 @@ export default class extends Controller {
|
|
|
222
305
|
|
|
223
306
|
})
|
|
224
307
|
.finally(() => {
|
|
225
|
-
this.loadingOlder = false
|
|
308
|
+
if (this.isCurrentPaginationContext(requestContext)) this.loadingOlder = false
|
|
226
309
|
})
|
|
227
310
|
}
|
|
228
311
|
|
|
@@ -234,10 +317,12 @@ export default class extends Controller {
|
|
|
234
317
|
return
|
|
235
318
|
}
|
|
236
319
|
|
|
320
|
+
const requestContext = this.paginationRequestContext()
|
|
237
321
|
this.loadingNewer = true
|
|
238
322
|
|
|
239
|
-
this.fetchComments({ after_id: maxId })
|
|
323
|
+
this.fetchComments({ after_id: maxId }, { pagination: true })
|
|
240
324
|
.then((html) => {
|
|
325
|
+
if (!this.isCurrentPaginationContext(requestContext)) return
|
|
241
326
|
if (html.trim() === '') {
|
|
242
327
|
|
|
243
328
|
this.allNewerLoaded = true
|
|
@@ -246,13 +331,34 @@ export default class extends Controller {
|
|
|
246
331
|
// Append to end (Visual Bottom)
|
|
247
332
|
this.listTarget.insertAdjacentHTML('beforeend', html)
|
|
248
333
|
renderMarkdownInContainer(this.listTarget)
|
|
334
|
+
const addedTopic = this.recordRenderedAllTopicWatermarks(
|
|
335
|
+
this.listTarget.querySelectorAll('.comment-item'),
|
|
336
|
+
{ includeNewTopics: true },
|
|
337
|
+
)
|
|
338
|
+
if (addedTopic) this.reportRenderedAllTopics()
|
|
339
|
+
this.markCommentsRead()
|
|
249
340
|
})
|
|
250
341
|
.finally(() => {
|
|
251
|
-
this.loadingNewer = false
|
|
342
|
+
if (this.isCurrentPaginationContext(requestContext)) this.loadingNewer = false
|
|
252
343
|
})
|
|
253
344
|
}
|
|
254
345
|
|
|
255
|
-
|
|
346
|
+
paginationRequestContext() {
|
|
347
|
+
return {
|
|
348
|
+
creativeId: this.creativeId,
|
|
349
|
+
topicId: this.currentTopicId || null,
|
|
350
|
+
loadVersion: this._loadCommentsVersion,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
isCurrentPaginationContext({ creativeId, topicId, loadVersion }) {
|
|
355
|
+
return this.element.isConnected &&
|
|
356
|
+
this.creativeId === creativeId &&
|
|
357
|
+
(this.currentTopicId || null) === topicId &&
|
|
358
|
+
this._loadCommentsVersion === loadVersion
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fetchComments(params = {}, { loadVersion, pagination = false } = {}) {
|
|
256
362
|
const urlParams = new URLSearchParams(params)
|
|
257
363
|
if (this.manualSearchQuery) {
|
|
258
364
|
urlParams.set('search', this.manualSearchQuery)
|
|
@@ -277,7 +383,27 @@ export default class extends Controller {
|
|
|
277
383
|
}
|
|
278
384
|
|
|
279
385
|
const serverTopicId = response.headers.get("X-Topic-Id")
|
|
280
|
-
|
|
386
|
+
const renderedTopicIds = response.headers.get("X-Rendered-Topic-Ids")
|
|
387
|
+
const renderedTopicWatermarks = response.headers.get("X-Rendered-Topic-Watermarks")
|
|
388
|
+
const superseded = loadVersion !== undefined && loadVersion !== this._loadCommentsVersion
|
|
389
|
+
// Only replace this snapshot for a full list load. Pagination requests
|
|
390
|
+
// happen later and may observe a topic-strip archive change without
|
|
391
|
+
// rendering that topic's existing history.
|
|
392
|
+
if (loadVersion !== undefined && !pagination && !superseded && !this.currentTopicId && renderedTopicIds !== null) {
|
|
393
|
+
this.renderedAllTopicIds = renderedTopicIds.split(',').filter(Boolean)
|
|
394
|
+
try {
|
|
395
|
+
this.renderedAllTopicWatermarks = renderedTopicWatermarks ? JSON.parse(renderedTopicWatermarks) : null
|
|
396
|
+
this.renderedAllIncludesLegacy = Boolean(this.renderedAllTopicWatermarks && Object.hasOwn(this.renderedAllTopicWatermarks, '_legacy'))
|
|
397
|
+
} catch (_error) {
|
|
398
|
+
this.renderedAllTopicWatermarks = null
|
|
399
|
+
this.renderedAllIncludesLegacy = false
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// A load superseded while in flight must not retopic anything: its own HTML
|
|
403
|
+
// is dropped, so moving currentTopicId (and the strip, and the form) to its
|
|
404
|
+
// answer would leave the surviving load rendering into a selection it never
|
|
405
|
+
// asked for.
|
|
406
|
+
if (serverTopicId !== null && serverTopicId !== undefined && !pagination && !superseded) {
|
|
281
407
|
// Server says we are in this topic.
|
|
282
408
|
// If it differs from current, update state.
|
|
283
409
|
|
|
@@ -287,6 +413,12 @@ export default class extends Controller {
|
|
|
287
413
|
|
|
288
414
|
if (currentStr !== serverStr) {
|
|
289
415
|
this.currentTopicId = serverTopicId
|
|
416
|
+
// Tell loadInitialComments' stale-topic guard that this switch is the
|
|
417
|
+
// answer to the load it is still awaiting. Stamp the version of the
|
|
418
|
+
// request that actually carried the header, not the controller's
|
|
419
|
+
// latest: reading the latest would hand the exemption to a newer load
|
|
420
|
+
// that never asked for a topic switch.
|
|
421
|
+
this._serverTopicRequestVersion = loadVersion
|
|
290
422
|
// Notify topics controller to update UI
|
|
291
423
|
const event = new CustomEvent("comments--topics:update-selection", { detail: { topicId: serverTopicId } })
|
|
292
424
|
window.dispatchEvent(event)
|
|
@@ -296,8 +428,15 @@ export default class extends Controller {
|
|
|
296
428
|
// Deep link / around_comment_id resolution from server must win over
|
|
297
429
|
// saved topic state for the current popup session.
|
|
298
430
|
this.popupController.topicsController.setOverrideTopicId(serverTopicId)
|
|
299
|
-
//
|
|
300
|
-
|
|
431
|
+
// Go through selectTopic, not updateSelectionUI: a deep link can resolve
|
|
432
|
+
// to an archived topic, whose chip exists only while the archived section
|
|
433
|
+
// is expanded, and form_controller learns the active topic solely from the
|
|
434
|
+
// change event — without it a reply posts into the previously selected
|
|
435
|
+
// conversation. The reload this event would normally trigger is already
|
|
436
|
+
// ruled out: this.currentTopicId was set to serverTopicId just above, so
|
|
437
|
+
// handleTopicChange's equality guard returns before resetToLatest() can
|
|
438
|
+
// discard the highlight window.
|
|
439
|
+
this.popupController.topicsController.selectTopic(serverTopicId)
|
|
301
440
|
|
|
302
441
|
// Also update data attribute for CSS scoping
|
|
303
442
|
this.listTarget.dataset.currentTopicId = serverTopicId || ""
|
|
@@ -312,6 +451,7 @@ export default class extends Controller {
|
|
|
312
451
|
}
|
|
313
452
|
|
|
314
453
|
applySearchQuery(query) {
|
|
454
|
+
this.flushPendingRead()
|
|
315
455
|
this.resetState()
|
|
316
456
|
this.manualSearchQuery = query
|
|
317
457
|
this.listTarget.innerHTML = this.element.dataset.loadingText || '<div class="loading-spinner">Loading...</div>'
|
|
@@ -334,9 +474,17 @@ export default class extends Controller {
|
|
|
334
474
|
return parseInt(last.dataset.commentId)
|
|
335
475
|
}
|
|
336
476
|
|
|
477
|
+
// Public seam for sibling controllers that scroll the list on their own
|
|
478
|
+
// (e.g. the review-quote chip). Lets them drop the prev-message anchor at the
|
|
479
|
+
// choke point without reaching into the navigator's internals.
|
|
480
|
+
notifyProgrammaticScroll() {
|
|
481
|
+
this.prevMsgNavigator?.notifyProgrammaticScroll()
|
|
482
|
+
}
|
|
483
|
+
|
|
337
484
|
highlightComment(commentId) {
|
|
338
485
|
const comment = document.getElementById(`comment_${commentId}`)
|
|
339
486
|
if (!comment) return
|
|
487
|
+
this.prevMsgNavigator?.notifyProgrammaticScroll()
|
|
340
488
|
comment.scrollIntoView({ behavior: 'auto', block: 'center' })
|
|
341
489
|
comment.classList.add('highlight-flash')
|
|
342
490
|
comment.dataset.highlighted = 'true'
|
|
@@ -345,18 +493,67 @@ export default class extends Controller {
|
|
|
345
493
|
|
|
346
494
|
markCommentsRead() {
|
|
347
495
|
if (!this.creativeId) return
|
|
348
|
-
window.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
496
|
+
if (this.markReadTimeout) window.clearTimeout(this.markReadTimeout)
|
|
497
|
+
const creativeId = this.creativeId
|
|
498
|
+
const topicId = this.currentTopicId || null
|
|
499
|
+
const topicIds = topicId ? null : this.renderedAllTopicIds
|
|
500
|
+
const topicWatermarks = topicId ? null : this.renderedAllTopicWatermarks
|
|
501
|
+
const pendingRead = { creativeId, topicId, topicIds, topicWatermarks }
|
|
502
|
+
this.pendingRead = pendingRead
|
|
503
|
+
this.markReadTimeout = window.setTimeout(() => {
|
|
504
|
+
this.markReadTimeout = null
|
|
505
|
+
if (this.pendingRead !== pendingRead) return
|
|
506
|
+
this.pendingRead = null
|
|
507
|
+
if (!this.element.isConnected || this.creativeId !== creativeId || (this.currentTopicId || null) !== topicId) return
|
|
508
|
+
|
|
509
|
+
this.updateReadPointer(creativeId, topicId, topicIds, topicWatermarks)
|
|
357
510
|
}, 2000);
|
|
358
511
|
}
|
|
359
512
|
|
|
513
|
+
flushPendingRead({ keepalive = false } = {}) {
|
|
514
|
+
const pendingRead = this.pendingRead
|
|
515
|
+
if (!pendingRead) return
|
|
516
|
+
|
|
517
|
+
if (this.markReadTimeout) window.clearTimeout(this.markReadTimeout)
|
|
518
|
+
this.markReadTimeout = null
|
|
519
|
+
this.pendingRead = null
|
|
520
|
+
this.updateReadPointer(
|
|
521
|
+
pendingRead.creativeId,
|
|
522
|
+
pendingRead.topicId,
|
|
523
|
+
pendingRead.topicIds,
|
|
524
|
+
pendingRead.topicWatermarks,
|
|
525
|
+
{ keepalive }
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
updateReadPointer(creativeId, topicId, topicIds = null, topicWatermarks = null, { keepalive = false } = {}) {
|
|
530
|
+
if (!creativeId) return
|
|
531
|
+
|
|
532
|
+
csrfFetch('/comment_read_pointers/update', {
|
|
533
|
+
method: 'POST',
|
|
534
|
+
headers: {
|
|
535
|
+
'Content-Type': 'application/json',
|
|
536
|
+
},
|
|
537
|
+
keepalive,
|
|
538
|
+
body: JSON.stringify({
|
|
539
|
+
creative_id: creativeId,
|
|
540
|
+
topic_id: topicId,
|
|
541
|
+
...(topicIds ? { topic_ids: topicIds } : {}),
|
|
542
|
+
...(topicWatermarks ? { topic_watermarks: topicWatermarks } : {}),
|
|
543
|
+
}),
|
|
544
|
+
}).then((response) => {
|
|
545
|
+
if (!response.ok || !this.element.isConnected || this.creativeId !== creativeId || (this.currentTopicId || null) !== topicId) return
|
|
546
|
+
|
|
547
|
+
// A successful topic read changes its chip and the aggregate creative
|
|
548
|
+
// badge. Reload immediately instead of leaving stale counts on screen.
|
|
549
|
+
this.popupController?.topicsController?.loadTopics?.()
|
|
550
|
+
}).catch(() => { /* ignore — creative may have been deleted */ })
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
handlePrevMsgUserInput() {
|
|
554
|
+
this.prevMsgNavigator.notifyUserInput()
|
|
555
|
+
}
|
|
556
|
+
|
|
360
557
|
handleScroll() {
|
|
361
558
|
if (!this.initialLoadComplete) return
|
|
362
559
|
|
|
@@ -459,23 +656,104 @@ export default class extends Controller {
|
|
|
459
656
|
// Actually form_controller handleSubmit calls this list controller? No, distinct.
|
|
460
657
|
}
|
|
461
658
|
|
|
659
|
+
// A comment with no topic is never archived, so the empty check also keeps
|
|
660
|
+
// this off the topics controller for the common case.
|
|
661
|
+
isArchivedTopicMessage(topicId) {
|
|
662
|
+
if (!topicId) return false
|
|
663
|
+
return Boolean(this.popupController?.topicsController?.isArchivedTopic?.(topicId))
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// All Messages sends a bounded per-topic snapshot so a later archive change
|
|
667
|
+
// cannot mark comments the list did not render as read. A visible live append
|
|
668
|
+
// extends that bound for its already-rendered topic before the read debounce
|
|
669
|
+
// captures it. Only pagination may add a new topic: its comment is already
|
|
670
|
+
// visible in the DOM. Live streams still fence topics outside the initial
|
|
671
|
+
// snapshot because older history for those topics may be unseen.
|
|
672
|
+
recordRenderedAllTopicWatermarks(comments, { includeNewTopics = false } = {}) {
|
|
673
|
+
if (this.currentTopicId || !Array.isArray(this.renderedAllTopicIds)) return false
|
|
674
|
+
|
|
675
|
+
let addedTopic = false
|
|
676
|
+
const renderedComments = comments instanceof Element ? [comments] : Array.from(comments || [])
|
|
677
|
+
renderedComments.forEach((comment) => {
|
|
678
|
+
const topicId = comment.dataset.topicId
|
|
679
|
+
const commentId = Number.parseInt(comment.dataset.commentId, 10)
|
|
680
|
+
if (!Number.isSafeInteger(commentId) || commentId <= 0) return
|
|
681
|
+
|
|
682
|
+
const watermarkKey = topicId || '_legacy'
|
|
683
|
+
if (!topicId && !this.renderedAllIncludesLegacy) {
|
|
684
|
+
if (!includeNewTopics) return
|
|
685
|
+
|
|
686
|
+
this.renderedAllIncludesLegacy = true
|
|
687
|
+
addedTopic = true
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (topicId && !this.renderedAllTopicIds.some((id) => String(id) === String(topicId))) {
|
|
691
|
+
if (!includeNewTopics) return
|
|
692
|
+
|
|
693
|
+
this.renderedAllTopicIds.push(String(topicId))
|
|
694
|
+
addedTopic = true
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
this.renderedAllTopicWatermarks ||= {}
|
|
698
|
+
const previousId = Number.parseInt(this.renderedAllTopicWatermarks[watermarkKey], 10)
|
|
699
|
+
if (!Number.isSafeInteger(previousId) || commentId > previousId) {
|
|
700
|
+
this.renderedAllTopicWatermarks[watermarkKey] = commentId
|
|
701
|
+
}
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
return addedTopic
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
reportRenderedAllTopics() {
|
|
708
|
+
if (this.currentTopicId || !Array.isArray(this.renderedAllTopicIds)) return
|
|
709
|
+
|
|
710
|
+
this.element.dispatchEvent(new CustomEvent('comments--list:rendered-all-topics', {
|
|
711
|
+
bubbles: true,
|
|
712
|
+
detail: {
|
|
713
|
+
creativeId: this.creativeId,
|
|
714
|
+
topicIds: this.renderedAllTopicIds,
|
|
715
|
+
includesLegacy: this.renderedAllIncludesLegacy,
|
|
716
|
+
},
|
|
717
|
+
}))
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
isOutsideRenderedAllTopics(topicId) {
|
|
721
|
+
return Array.isArray(this.renderedAllTopicIds) &&
|
|
722
|
+
!this.renderedAllTopicIds.some((id) => String(id) === String(topicId))
|
|
723
|
+
}
|
|
724
|
+
|
|
462
725
|
handleStreamRender(event) {
|
|
463
726
|
// Only care about streams targeting our list
|
|
464
727
|
if (event.target.target !== 'comments-list') return
|
|
465
728
|
|
|
466
729
|
// Deduplication: If manually appended by form_controller, block the stream echo.
|
|
730
|
+
let appendedComment = null
|
|
467
731
|
if (event.target.action === 'append') {
|
|
468
732
|
const templateContent = event.target.templateContent || event.target.querySelector('template')?.content
|
|
469
733
|
const firstChild = templateContent?.firstElementChild
|
|
734
|
+
appendedComment = firstChild
|
|
470
735
|
|
|
471
|
-
// Check for topic context mismatch
|
|
736
|
+
// Check for topic context mismatch. Runs ahead of the search block below:
|
|
737
|
+
// both block the append, but only this one badges, and the badge is the
|
|
738
|
+
// sole notice that an out-of-view conversation moved. Search suppressing
|
|
739
|
+
// it would hide an archived topic's traffic completely, since the archived
|
|
740
|
+
// section carries no other signal.
|
|
472
741
|
if (firstChild && firstChild.dataset.topicId !== undefined) {
|
|
473
742
|
const messageTopicId = firstChild.dataset.topicId
|
|
474
743
|
const currentTopicId = this.currentTopicId || ""
|
|
475
744
|
|
|
476
745
|
// If we are in a specific topic (currentTopicId is set)
|
|
477
|
-
// AND the message is for a different topic
|
|
478
|
-
|
|
746
|
+
// AND the message is for a different topic.
|
|
747
|
+
//
|
|
748
|
+
// All Messages (currentTopicId === "") takes everything except archived
|
|
749
|
+
// topics: CommentsController#index filters those out of this view, so
|
|
750
|
+
// letting a live one in would show a message that vanishes on reload.
|
|
751
|
+
// Both cases badge the topic instead of appending.
|
|
752
|
+
const isForeignTopic = currentTopicId
|
|
753
|
+
? String(currentTopicId) !== String(messageTopicId)
|
|
754
|
+
: this.isArchivedTopicMessage(messageTopicId) || this.isOutsideRenderedAllTopics(messageTopicId)
|
|
755
|
+
|
|
756
|
+
if (isForeignTopic) {
|
|
479
757
|
event.preventDefault()
|
|
480
758
|
// Dispatch event for topics controller to show badge
|
|
481
759
|
const customEvent = new CustomEvent("comments--topics:new-message", {
|
|
@@ -486,6 +764,17 @@ export default class extends Controller {
|
|
|
486
764
|
}
|
|
487
765
|
}
|
|
488
766
|
|
|
767
|
+
// A search-filtered list is the result set of a query, and the match runs
|
|
768
|
+
// server-side over the raw content. A live append carries no verdict on
|
|
769
|
+
// whether it matches, so letting it in drops an unrelated message into the
|
|
770
|
+
// results — and, when there were none, takes the "no results" notice with
|
|
771
|
+
// it (comments--placeholder clears on any .comment-item arriving). Blocked
|
|
772
|
+
// like history mode below; the next search or reset re-queries the server.
|
|
773
|
+
if (this.manualSearchQuery) {
|
|
774
|
+
event.preventDefault()
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
|
|
489
778
|
if (firstChild && firstChild.id && document.getElementById(firstChild.id)) {
|
|
490
779
|
event.preventDefault()
|
|
491
780
|
return
|
|
@@ -501,7 +790,13 @@ export default class extends Controller {
|
|
|
501
790
|
// Optional: Show a "New messages" indicator?
|
|
502
791
|
// For now, strict requirement: "do not add to DOM".
|
|
503
792
|
} else {
|
|
504
|
-
|
|
793
|
+
// The append is about to become visible. Mark it read after the existing
|
|
794
|
+
// debounce, so a reconnect or popup close cannot turn a viewed live
|
|
795
|
+
// message back into an unread one.
|
|
796
|
+
if (event.target.action === 'append') {
|
|
797
|
+
this.recordRenderedAllTopicWatermarks(appendedComment)
|
|
798
|
+
this.markCommentsRead()
|
|
799
|
+
}
|
|
505
800
|
}
|
|
506
801
|
}
|
|
507
802
|
|
|
@@ -696,9 +991,8 @@ export default class extends Controller {
|
|
|
696
991
|
btnRect,
|
|
697
992
|
(topic) => {
|
|
698
993
|
if (topic.created) {
|
|
699
|
-
// Topic was just created with comments moved — refresh
|
|
700
994
|
this.clearSelection()
|
|
701
|
-
this.
|
|
995
|
+
this.switchToTopic(topic.id)
|
|
702
996
|
} else {
|
|
703
997
|
this.handleMoveToTopic({ detail: { commentIds, targetTopicId: topic.id } })
|
|
704
998
|
}
|
|
@@ -890,7 +1184,7 @@ export default class extends Controller {
|
|
|
890
1184
|
if (!topicId) return
|
|
891
1185
|
const topicsController = this.popupController?.topicsController
|
|
892
1186
|
if (topicsController?.selectTopic) {
|
|
893
|
-
topicsController.selectTopic(topicId)
|
|
1187
|
+
topicsController.selectTopic(topicId, { userInitiated: true })
|
|
894
1188
|
} else {
|
|
895
1189
|
this.currentTopicId = topicId
|
|
896
1190
|
this.resetToLatest()
|
|
@@ -1078,28 +1372,16 @@ export default class extends Controller {
|
|
|
1078
1372
|
|
|
1079
1373
|
scrollToPreviousMessage() {
|
|
1080
1374
|
const list = this.listTarget
|
|
1081
|
-
const
|
|
1082
|
-
if (
|
|
1083
|
-
|
|
1084
|
-
const listRect = list.getBoundingClientRect()
|
|
1085
|
-
const viewportTop = listRect.top
|
|
1086
|
-
|
|
1087
|
-
let currentIdx = -1
|
|
1088
|
-
for (let i = 0; i < items.length; i++) {
|
|
1089
|
-
const rect = items[i].getBoundingClientRect()
|
|
1090
|
-
if (rect.top >= viewportTop - 2) {
|
|
1091
|
-
currentIdx = i
|
|
1092
|
-
break
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
if (currentIdx === -1) currentIdx = items.length - 1
|
|
1375
|
+
const elements = Array.from(list.querySelectorAll('.comment-item'))
|
|
1376
|
+
if (elements.length === 0) return
|
|
1097
1377
|
|
|
1098
|
-
const
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1378
|
+
const viewportTop = list.getBoundingClientRect().top
|
|
1379
|
+
const measured = elements.map((el) => ({
|
|
1380
|
+
id: el.dataset.commentId,
|
|
1381
|
+
top: el.getBoundingClientRect().top,
|
|
1382
|
+
}))
|
|
1101
1383
|
|
|
1102
|
-
const targetIdx =
|
|
1384
|
+
const targetIdx = this.prevMsgNavigator.resolveTargetIndex(measured, viewportTop)
|
|
1103
1385
|
if (targetIdx < 0) {
|
|
1104
1386
|
if (!this.allOlderLoaded) {
|
|
1105
1387
|
this.loadOlderComments()
|
|
@@ -1107,8 +1389,9 @@ export default class extends Controller {
|
|
|
1107
1389
|
return
|
|
1108
1390
|
}
|
|
1109
1391
|
|
|
1110
|
-
const target =
|
|
1392
|
+
const target = elements[targetIdx]
|
|
1111
1393
|
const targetTop = target.offsetTop - list.offsetTop
|
|
1394
|
+
this.prevMsgNavigator.commit(measured[targetIdx].id, measured[targetIdx].top)
|
|
1112
1395
|
list.scrollTo({ top: targetTop, behavior: 'smooth' })
|
|
1113
1396
|
this.stickToBottom = false
|
|
1114
1397
|
|
|
@@ -1126,6 +1409,9 @@ export default class extends Controller {
|
|
|
1126
1409
|
}
|
|
1127
1410
|
|
|
1128
1411
|
scrollToBottom() {
|
|
1412
|
+
// A jump to latest we did not initiate via the button - drop the anchor so
|
|
1413
|
+
// the next previous-message click resolves from here, not a stale target.
|
|
1414
|
+
this.prevMsgNavigator?.notifyProgrammaticScroll()
|
|
1129
1415
|
// In column reverse, bottom of scroll might be tricky.
|
|
1130
1416
|
// Easiest is to set scrollTop to a large value.
|
|
1131
1417
|
requestAnimationFrame(() => {
|