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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals'
|
|
6
|
+
import { Application } from '@hotwired/stimulus'
|
|
7
|
+
import FormController from '../form_controller'
|
|
8
|
+
import chatDrafts from '../../../lib/chat_drafts'
|
|
9
|
+
|
|
10
|
+
describe('FormController - IME composition send', () => {
|
|
11
|
+
let application
|
|
12
|
+
let container
|
|
13
|
+
let controller
|
|
14
|
+
let popupEl
|
|
15
|
+
|
|
16
|
+
const FIXTURE = `
|
|
17
|
+
<div id="comments-popup"
|
|
18
|
+
data-controller="comments--form"
|
|
19
|
+
data-update-comment-text="Update">
|
|
20
|
+
<form data-comments--form-target="form">
|
|
21
|
+
<input type="hidden" name="comment[quoted_comment_id]" data-comments--form-target="quotedCommentId" value="" />
|
|
22
|
+
<input type="hidden" name="comment[quoted_text]" data-comments--form-target="quotedText" value="" />
|
|
23
|
+
<div data-comments--form-target="quoteIndicator" style="display:none;">
|
|
24
|
+
<span data-comments--form-target="quoteIndicatorText"></span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="review-quotes-container" data-comments--form-target="reviewQuotesContainer" style="display:none;"></div>
|
|
27
|
+
<textarea data-comments--form-target="textarea" rows="2"></textarea>
|
|
28
|
+
<input type="checkbox" data-comments--form-target="privateCheckbox" />
|
|
29
|
+
<button type="submit" data-comments--form-target="submit">Send</button>
|
|
30
|
+
<button data-comments--form-target="cancel" style="display:none;">Cancel</button>
|
|
31
|
+
<button data-comments--form-target="searchButton" style="display:none;">Search</button>
|
|
32
|
+
<button data-comments--form-target="voiceButton" style="display:none;">Voice</button>
|
|
33
|
+
<input type="file" data-comments--form-target="imageInput" style="display:none;" />
|
|
34
|
+
<button data-comments--form-target="imageButton" style="display:none;">Image</button>
|
|
35
|
+
<div data-comments--form-target="attachmentList"></div>
|
|
36
|
+
</form>
|
|
37
|
+
</div>`
|
|
38
|
+
|
|
39
|
+
// Stimulus disconnects a removed controller asynchronously, so a previous
|
|
40
|
+
// test's pending draft flush can land inside a later test's timer wait.
|
|
41
|
+
// Giving each test its own chat id keeps those flushes off our key.
|
|
42
|
+
let chatId
|
|
43
|
+
let nextChatId = 77
|
|
44
|
+
|
|
45
|
+
const dispatchTopicChange = (effectiveCreativeId = chatId, topicId = '10073') => {
|
|
46
|
+
popupEl.dataset.effectiveCreativeId = String(effectiveCreativeId)
|
|
47
|
+
popupEl.dispatchEvent(
|
|
48
|
+
new CustomEvent('comments--topics:change', { detail: { topicId } }),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const typeInto = (textarea, value) => {
|
|
53
|
+
textarea.value = value
|
|
54
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A real IME commit ends the composition and then fires `input` for the
|
|
58
|
+
// committed text. jsdom has no IME, so replay that exact pair.
|
|
59
|
+
const commitComposition = (textarea, value = textarea.value) => {
|
|
60
|
+
textarea.dispatchEvent(new CompositionEvent('compositionend', { bubbles: true, data: value }))
|
|
61
|
+
textarea.value = value
|
|
62
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
window.localStorage.clear()
|
|
67
|
+
window.sessionStorage.clear()
|
|
68
|
+
document.body.dataset.currentUserId = '9'
|
|
69
|
+
|
|
70
|
+
if (typeof global.requestAnimationFrame !== 'function') {
|
|
71
|
+
global.requestAnimationFrame = (cb) => setTimeout(cb, 0)
|
|
72
|
+
}
|
|
73
|
+
const meta = document.createElement('meta')
|
|
74
|
+
meta.name = 'csrf-token'
|
|
75
|
+
meta.content = 'test-token'
|
|
76
|
+
document.head.appendChild(meta)
|
|
77
|
+
|
|
78
|
+
container = document.createElement('div')
|
|
79
|
+
container.innerHTML = FIXTURE
|
|
80
|
+
document.body.appendChild(container)
|
|
81
|
+
|
|
82
|
+
application = Application.start()
|
|
83
|
+
application.register('comments--form', FormController)
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
85
|
+
popupEl = container.querySelector('#comments-popup')
|
|
86
|
+
controller = application.getControllerForElementAndIdentifier(popupEl, 'comments--form')
|
|
87
|
+
jest.spyOn(controller, 'setImageFiles').mockImplementation(() => {})
|
|
88
|
+
chatId = String(nextChatId)
|
|
89
|
+
nextChatId += 1
|
|
90
|
+
controller.creativeId = chatId
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
application.stop()
|
|
95
|
+
document.body.removeChild(container)
|
|
96
|
+
document.head.querySelector('meta[name="csrf-token"]').remove()
|
|
97
|
+
delete document.body.dataset.currentUserId
|
|
98
|
+
jest.restoreAllMocks()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('Enter pressed while an IME composition is active does not send', async () => {
|
|
102
|
+
dispatchTopicChange()
|
|
103
|
+
global.fetch = jest.fn(() => Promise.resolve({
|
|
104
|
+
ok: true,
|
|
105
|
+
status: 200,
|
|
106
|
+
text: () => Promise.resolve('<div></div>'),
|
|
107
|
+
}))
|
|
108
|
+
typeInto(controller.textareaTarget, '안녕하세')
|
|
109
|
+
|
|
110
|
+
controller.textareaTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
|
111
|
+
key: 'Enter',
|
|
112
|
+
isComposing: true,
|
|
113
|
+
bubbles: true,
|
|
114
|
+
cancelable: true,
|
|
115
|
+
}))
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
117
|
+
|
|
118
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
119
|
+
expect(controller.textareaTarget.value).toBe('안녕하세')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('an IME commit after send does not restore the sent text into the textarea', async () => {
|
|
123
|
+
dispatchTopicChange()
|
|
124
|
+
let finishFetch
|
|
125
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
126
|
+
|
|
127
|
+
typeInto(controller.textareaTarget, '안녕하세요')
|
|
128
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
129
|
+
|
|
130
|
+
// The IME commits the pending syllable right after keydown started the
|
|
131
|
+
// send. The textarea content is unchanged, but an input event still fires.
|
|
132
|
+
commitComposition(controller.textareaTarget)
|
|
133
|
+
|
|
134
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
136
|
+
|
|
137
|
+
expect(controller.textareaTarget.value).toBe('')
|
|
138
|
+
expect(chatDrafts.get(chatId)).toBeNull()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('a slow send does not let the IME commit debounce the sent text into storage', async () => {
|
|
142
|
+
dispatchTopicChange()
|
|
143
|
+
let finishFetch
|
|
144
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
145
|
+
|
|
146
|
+
typeInto(controller.textareaTarget, '안녕하세요')
|
|
147
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
148
|
+
commitComposition(controller.textareaTarget)
|
|
149
|
+
|
|
150
|
+
// The response outlives the 500ms draft debounce window.
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
152
|
+
expect(chatDrafts.get(chatId)).toBeNull()
|
|
153
|
+
|
|
154
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
156
|
+
|
|
157
|
+
expect(controller.textareaTarget.value).toBe('')
|
|
158
|
+
expect(chatDrafts.get(chatId)).toBeNull()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('Enter after the composition ends still sends', async () => {
|
|
162
|
+
dispatchTopicChange()
|
|
163
|
+
global.fetch = jest.fn(() => Promise.resolve({
|
|
164
|
+
ok: true,
|
|
165
|
+
status: 200,
|
|
166
|
+
text: () => Promise.resolve('<div></div>'),
|
|
167
|
+
}))
|
|
168
|
+
typeInto(controller.textareaTarget, '안녕하세요')
|
|
169
|
+
|
|
170
|
+
controller.textareaTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
|
171
|
+
key: 'Enter',
|
|
172
|
+
isComposing: false,
|
|
173
|
+
bubbles: true,
|
|
174
|
+
cancelable: true,
|
|
175
|
+
}))
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
177
|
+
|
|
178
|
+
expect(global.fetch).toHaveBeenCalled()
|
|
179
|
+
expect(controller.textareaTarget.value).toBe('')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('re-entering the same text during the send is preserved, not treated as an IME commit', async () => {
|
|
183
|
+
dispatchTopicChange()
|
|
184
|
+
let finishFetch
|
|
185
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
186
|
+
|
|
187
|
+
typeInto(controller.textareaTarget, 'send me twice')
|
|
188
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
189
|
+
|
|
190
|
+
// Select-all + paste of the identical string to send it again. No
|
|
191
|
+
// composition is involved, so this is a genuine edit even though the
|
|
192
|
+
// value matches the in-flight submission.
|
|
193
|
+
typeInto(controller.textareaTarget, 'send me twice')
|
|
194
|
+
|
|
195
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
197
|
+
|
|
198
|
+
expect(controller.textareaTarget.value).toBe('send me twice')
|
|
199
|
+
expect(chatDrafts.get(chatId)).toBe('send me twice')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('an input event still carrying isComposing is treated as an IME commit', async () => {
|
|
203
|
+
dispatchTopicChange()
|
|
204
|
+
let finishFetch
|
|
205
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
206
|
+
|
|
207
|
+
typeInto(controller.textareaTarget, '안녕하세요')
|
|
208
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
209
|
+
|
|
210
|
+
// Some browsers fire the commit `input` before `compositionend`, with
|
|
211
|
+
// isComposing still set on the input event itself.
|
|
212
|
+
controller.textareaTarget.dispatchEvent(
|
|
213
|
+
new InputEvent('input', { bubbles: true, isComposing: true }),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
218
|
+
|
|
219
|
+
expect(controller.textareaTarget.value).toBe('')
|
|
220
|
+
expect(chatDrafts.get(chatId)).toBeNull()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test.each(['paste', 'drop'])('a compositionend that trails the commit input does not swallow a later %s', async (gesture) => {
|
|
224
|
+
dispatchTopicChange()
|
|
225
|
+
let finishFetch
|
|
226
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
227
|
+
|
|
228
|
+
typeInto(controller.textareaTarget, 'send me twice')
|
|
229
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
230
|
+
|
|
231
|
+
// Input-before-compositionend ordering: the commit input already identified
|
|
232
|
+
// itself, so the trailing compositionend has no input left to pair with.
|
|
233
|
+
controller.textareaTarget.dispatchEvent(
|
|
234
|
+
new InputEvent('input', { bubbles: true, isComposing: true }),
|
|
235
|
+
)
|
|
236
|
+
controller.textareaTarget.dispatchEvent(
|
|
237
|
+
new CompositionEvent('compositionend', { bubbles: true, data: 'send me twice' }),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Select-all + paste (or drag-drop) of the identical string to send it again.
|
|
241
|
+
controller.textareaTarget.dispatchEvent(new Event(gesture, { bubbles: true }))
|
|
242
|
+
typeInto(controller.textareaTarget, 'send me twice')
|
|
243
|
+
|
|
244
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
245
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
246
|
+
|
|
247
|
+
expect(controller.textareaTarget.value).toBe('send me twice')
|
|
248
|
+
expect(chatDrafts.get(chatId)).toBe('send me twice')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('a compositionend that trails the commit input does not swallow later typing', async () => {
|
|
252
|
+
dispatchTopicChange()
|
|
253
|
+
let finishFetch
|
|
254
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
255
|
+
|
|
256
|
+
typeInto(controller.textareaTarget, 'send me twice')
|
|
257
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
258
|
+
|
|
259
|
+
controller.textareaTarget.dispatchEvent(
|
|
260
|
+
new InputEvent('input', { bubbles: true, isComposing: true }),
|
|
261
|
+
)
|
|
262
|
+
controller.textareaTarget.dispatchEvent(
|
|
263
|
+
new CompositionEvent('compositionend', { bubbles: true, data: 'send me twice' }),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
// Retyping the same string by hand — an ordinary keystroke, not a commit.
|
|
267
|
+
controller.textareaTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
|
268
|
+
key: 'e',
|
|
269
|
+
isComposing: false,
|
|
270
|
+
bubbles: true,
|
|
271
|
+
cancelable: true,
|
|
272
|
+
}))
|
|
273
|
+
typeInto(controller.textareaTarget, 'send me twice')
|
|
274
|
+
|
|
275
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
276
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
277
|
+
|
|
278
|
+
expect(controller.textareaTarget.value).toBe('send me twice')
|
|
279
|
+
expect(chatDrafts.get(chatId)).toBe('send me twice')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('a composing keydown between compositionend and the commit input keeps the latch', async () => {
|
|
283
|
+
dispatchTopicChange()
|
|
284
|
+
let finishFetch
|
|
285
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
286
|
+
|
|
287
|
+
typeInto(controller.textareaTarget, '안녕하세요')
|
|
288
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
289
|
+
|
|
290
|
+
controller.textareaTarget.dispatchEvent(
|
|
291
|
+
new CompositionEvent('compositionend', { bubbles: true, data: '안녕하세요' }),
|
|
292
|
+
)
|
|
293
|
+
// Soft keyboards can interleave the next composing keystroke before the
|
|
294
|
+
// commit input lands; that is not a fresh gesture, so it must not expire.
|
|
295
|
+
controller.textareaTarget.dispatchEvent(new KeyboardEvent('keydown', {
|
|
296
|
+
keyCode: 229,
|
|
297
|
+
bubbles: true,
|
|
298
|
+
cancelable: true,
|
|
299
|
+
}))
|
|
300
|
+
controller.textareaTarget.dispatchEvent(new Event('input', { bubbles: true }))
|
|
301
|
+
|
|
302
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
304
|
+
|
|
305
|
+
expect(controller.textareaTarget.value).toBe('')
|
|
306
|
+
expect(chatDrafts.get(chatId)).toBeNull()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('text genuinely typed during the send is still preserved', async () => {
|
|
310
|
+
dispatchTopicChange()
|
|
311
|
+
let finishFetch
|
|
312
|
+
global.fetch = jest.fn(() => new Promise((resolve) => { finishFetch = resolve }))
|
|
313
|
+
|
|
314
|
+
typeInto(controller.textareaTarget, 'first message')
|
|
315
|
+
controller.handleSend(new Event('submit', { cancelable: true }))
|
|
316
|
+
typeInto(controller.textareaTarget, 'typed while sending')
|
|
317
|
+
|
|
318
|
+
finishFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
319
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
320
|
+
|
|
321
|
+
expect(controller.textareaTarget.value).toBe('typed while sending')
|
|
322
|
+
expect(chatDrafts.get(chatId)).toBe('typed while sending')
|
|
323
|
+
})
|
|
324
|
+
})
|
|
@@ -282,7 +282,7 @@ describe('FormController - Review Quote Chips', () => {
|
|
|
282
282
|
|
|
283
283
|
expect(getQuotes(controller)).toHaveLength(0)
|
|
284
284
|
expect(getActiveId(controller)).toBeNull()
|
|
285
|
-
expect(controller.textareaTarget.placeholder).toBe('')
|
|
285
|
+
expect(controller.textareaTarget.placeholder).toBe('Type a message, or use / for commands')
|
|
286
286
|
expect(container.querySelector('.review-quotes-container').style.display).toBe('none')
|
|
287
287
|
})
|
|
288
288
|
|
|
@@ -306,7 +306,7 @@ describe('FormController - Review Quote Chips', () => {
|
|
|
306
306
|
|
|
307
307
|
expect(getQuotes(controller)).toHaveLength(0)
|
|
308
308
|
expect(getActiveId(controller)).toBeNull()
|
|
309
|
-
expect(controller.textareaTarget.placeholder).toBe('')
|
|
309
|
+
expect(controller.textareaTarget.placeholder).toBe('Type a message, or use / for commands')
|
|
310
310
|
})
|
|
311
311
|
})
|
|
312
312
|
|
|
@@ -343,4 +343,30 @@ describe('FormController - Review Quote Chips', () => {
|
|
|
343
343
|
expect(controller.textareaTarget.value).toBe('fb2')
|
|
344
344
|
})
|
|
345
345
|
})
|
|
346
|
+
|
|
347
|
+
describe('active chip re-click - programmatic scroll', () => {
|
|
348
|
+
// Re-clicking the active chip scrolls its quoted comment into view. That is a
|
|
349
|
+
// programmatic list scroll, so it must drop the prev-message anchor the same way
|
|
350
|
+
// scrollToBottom/highlightComment do — otherwise a stale anchor inside the corridor
|
|
351
|
+
// survives and the next previous-message click skips the comment now in view.
|
|
352
|
+
test('re-clicking the active chip notifies the list of a programmatic scroll', () => {
|
|
353
|
+
const commentEl = document.createElement('div')
|
|
354
|
+
commentEl.setAttribute('data-comment-id', '1')
|
|
355
|
+
commentEl.scrollIntoView = jest.fn() // jsdom has no scrollIntoView
|
|
356
|
+
document.body.appendChild(commentEl)
|
|
357
|
+
|
|
358
|
+
const notifyProgrammaticScroll = jest.fn()
|
|
359
|
+
jest
|
|
360
|
+
.spyOn(controller, 'listController', 'get')
|
|
361
|
+
.mockReturnValue({ notifyProgrammaticScroll })
|
|
362
|
+
|
|
363
|
+
controller.appendReviewQuote(1, 'first') // freshly appended quote is active
|
|
364
|
+
container.querySelector('.review-quote-chip-text').click()
|
|
365
|
+
|
|
366
|
+
expect(commentEl.scrollIntoView).toHaveBeenCalled()
|
|
367
|
+
expect(notifyProgrammaticScroll).toHaveBeenCalled()
|
|
368
|
+
|
|
369
|
+
commentEl.remove()
|
|
370
|
+
})
|
|
371
|
+
})
|
|
346
372
|
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals'
|
|
6
|
+
import CommentsFormController from '../form_controller'
|
|
7
|
+
|
|
8
|
+
const COMMENT_HTML =
|
|
9
|
+
'<div id="comment_9" class="comment-item" data-comment-id="9">hello team</div>'
|
|
10
|
+
|
|
11
|
+
// renderCommentHtml reaches for #comments-list in the document and for the
|
|
12
|
+
// sibling list controller through the `listController` getter. Stub both.
|
|
13
|
+
const buildController = ({ manualSearchQuery = null } = {}) => {
|
|
14
|
+
document.body.innerHTML = `
|
|
15
|
+
<div id="comments-list">
|
|
16
|
+
<div id="no-search-results" class="comments-placeholder">no results</div>
|
|
17
|
+
</div>
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
const listCtrl = {
|
|
21
|
+
manualSearchQuery,
|
|
22
|
+
resetToLatest: jest.fn(),
|
|
23
|
+
markCommentsRead: jest.fn(),
|
|
24
|
+
recordRenderedAllTopicWatermarks: jest.fn(),
|
|
25
|
+
reportRenderedAllTopics: jest.fn(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const controller = Object.create(CommentsFormController.prototype)
|
|
29
|
+
Object.defineProperty(controller, 'listController', { get: () => listCtrl })
|
|
30
|
+
|
|
31
|
+
return { controller, listCtrl }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const listItems = () =>
|
|
35
|
+
document.getElementById('comments-list').querySelectorAll('.comment-item')
|
|
36
|
+
|
|
37
|
+
describe('CommentsFormController local submit during search', () => {
|
|
38
|
+
test('leaves search and reloads rather than appending into the results', () => {
|
|
39
|
+
const { controller, listCtrl } = buildController({ manualSearchQuery: 'invoice' })
|
|
40
|
+
|
|
41
|
+
controller.renderCommentHtml(COMMENT_HTML)
|
|
42
|
+
|
|
43
|
+
// The posted comment carries no verdict on whether it matches the query,
|
|
44
|
+
// so it must not be spliced into the result set.
|
|
45
|
+
expect(listItems()).toHaveLength(0)
|
|
46
|
+
expect(listCtrl.resetToLatest).toHaveBeenCalledTimes(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('keeps the no-results notice instead of clearing it on submit', () => {
|
|
50
|
+
const { controller } = buildController({ manualSearchQuery: 'invoice' })
|
|
51
|
+
|
|
52
|
+
controller.renderCommentHtml(COMMENT_HTML)
|
|
53
|
+
|
|
54
|
+
// removePlaceholder() clears any .comments-placeholder, so reaching it
|
|
55
|
+
// would strip the explanation and leave an unrelated message alone on
|
|
56
|
+
// screen. resetToLatest() re-renders the list from the server instead.
|
|
57
|
+
expect(document.getElementById('no-search-results')).not.toBeNull()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('appends normally when no search is active', () => {
|
|
61
|
+
const { controller, listCtrl } = buildController({ manualSearchQuery: null })
|
|
62
|
+
|
|
63
|
+
controller.renderCommentHtml(COMMENT_HTML)
|
|
64
|
+
|
|
65
|
+
expect(listItems()).toHaveLength(1)
|
|
66
|
+
expect(listCtrl.resetToLatest).not.toHaveBeenCalled()
|
|
67
|
+
expect(listCtrl.recordRenderedAllTopicWatermarks).toHaveBeenCalledWith(
|
|
68
|
+
document.getElementById('comment_9'),
|
|
69
|
+
{ includeNewTopics: true },
|
|
70
|
+
)
|
|
71
|
+
// The placeholder is stale once a real comment lands.
|
|
72
|
+
expect(document.getElementById('no-search-results')).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('extends and reports the All Messages snapshot for a locally appended topic', () => {
|
|
76
|
+
const { controller, listCtrl } = buildController({ manualSearchQuery: null })
|
|
77
|
+
listCtrl.recordRenderedAllTopicWatermarks.mockReturnValue(true)
|
|
78
|
+
|
|
79
|
+
controller.renderCommentHtml(
|
|
80
|
+
'<div id="comment_10" class="comment-item" data-comment-id="10" data-topic-id="2">new topic</div>',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(listCtrl.recordRenderedAllTopicWatermarks).toHaveBeenCalledWith(
|
|
84
|
+
document.getElementById('comment_10'),
|
|
85
|
+
{ includeNewTopics: true },
|
|
86
|
+
)
|
|
87
|
+
expect(listCtrl.reportRenderedAllTopics).toHaveBeenCalledTimes(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('still replaces an edited comment in place during a search', () => {
|
|
91
|
+
// Mirrors the stream path, which blocks `append` but never `replace` /
|
|
92
|
+
// `remove`: the edited comment is already part of the result set, so
|
|
93
|
+
// dropping the update would leave stale text on screen.
|
|
94
|
+
const { controller, listCtrl } = buildController({ manualSearchQuery: 'invoice' })
|
|
95
|
+
document.getElementById('comments-list').insertAdjacentHTML(
|
|
96
|
+
'beforeend',
|
|
97
|
+
'<div id="comment_9" class="comment-item" data-comment-id="9">old text</div>',
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
controller.renderCommentHtml(COMMENT_HTML, { replaceExisting: true })
|
|
101
|
+
|
|
102
|
+
expect(listCtrl.resetToLatest).not.toHaveBeenCalled()
|
|
103
|
+
expect(document.getElementById('comment_9').textContent).toBe('hello team')
|
|
104
|
+
expect(listItems()).toHaveLength(1)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals'
|
|
6
|
+
import CommentsFormController from '../form_controller'
|
|
7
|
+
|
|
8
|
+
// A slash command with an input schema takes over the textarea and submits
|
|
9
|
+
// itself (modules/command_menu.js `argsForm.onSubmit`), so a draft the user had
|
|
10
|
+
// already typed cannot go out with it. The menu hands the draft over on a
|
|
11
|
+
// `comments--form:stash-draft` event and the controller puts it back once the
|
|
12
|
+
// send settles.
|
|
13
|
+
const buildController = () => {
|
|
14
|
+
document.body.innerHTML = '<div id="comments-popup"><textarea></textarea></div>'
|
|
15
|
+
const textarea = document.querySelector('textarea')
|
|
16
|
+
|
|
17
|
+
const controller = Object.create(CommentsFormController.prototype)
|
|
18
|
+
Object.defineProperty(controller, 'textareaTarget', { get: () => textarea })
|
|
19
|
+
controller._autoResize = jest.fn()
|
|
20
|
+
controller._updateSubmitButton = jest.fn()
|
|
21
|
+
controller.creativeId = '7'
|
|
22
|
+
|
|
23
|
+
return { controller, textarea }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The stash is tagged with the creative it was typed in, so tests build it the
|
|
27
|
+
// way handleStashDraft does rather than assigning a bare string.
|
|
28
|
+
const stash = (controller, draft) => {
|
|
29
|
+
controller._stashedDraft = { draft, creativeId: controller.creativeId }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('CommentsFormController stashed draft', () => {
|
|
33
|
+
test('stashes the draft carried by the event', () => {
|
|
34
|
+
const { controller } = buildController()
|
|
35
|
+
|
|
36
|
+
controller.handleStashDraft(
|
|
37
|
+
new CustomEvent('comments--form:stash-draft', { detail: { draft: 'roadmap notes' } }),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect(controller._stashedDraft).toEqual({ draft: 'roadmap notes', creativeId: '7' })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('receives the event dispatched on the textarea, which bubbles to the popup', () => {
|
|
44
|
+
// command_menu.js dispatches on `#new-comment-form textarea`, and the
|
|
45
|
+
// comments--form controller element is `#comments-popup`, which wraps the
|
|
46
|
+
// form (_comments_popup.html.erb). The stash only arrives if it bubbles.
|
|
47
|
+
const { controller, textarea } = buildController()
|
|
48
|
+
const element = document.getElementById('comments-popup')
|
|
49
|
+
element.addEventListener(
|
|
50
|
+
'comments--form:stash-draft',
|
|
51
|
+
controller.handleStashDraft.bind(controller),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
textarea.dispatchEvent(
|
|
55
|
+
new CustomEvent('comments--form:stash-draft', {
|
|
56
|
+
bubbles: true,
|
|
57
|
+
detail: { draft: 'roadmap notes' },
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(controller._stashedDraft).toEqual({ draft: 'roadmap notes', creativeId: '7' })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('restores the draft into the textarea the send cleared', () => {
|
|
65
|
+
const { controller, textarea } = buildController()
|
|
66
|
+
stash(controller, 'roadmap notes')
|
|
67
|
+
|
|
68
|
+
// The success path runs resetForm() before .finally(), so the box is empty.
|
|
69
|
+
textarea.value = ''
|
|
70
|
+
controller._restoreStashedDraft()
|
|
71
|
+
|
|
72
|
+
expect(textarea.value).toBe('roadmap notes')
|
|
73
|
+
expect(controller._autoResize).toHaveBeenCalledTimes(1)
|
|
74
|
+
expect(controller._updateSubmitButton).toHaveBeenCalledTimes(1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('does not clobber text the user typed while the command was in flight', () => {
|
|
78
|
+
const { controller, textarea } = buildController()
|
|
79
|
+
stash(controller, 'roadmap notes')
|
|
80
|
+
|
|
81
|
+
textarea.value = 'something else entirely'
|
|
82
|
+
controller._restoreStashedDraft('/calendar 2026-08-14')
|
|
83
|
+
|
|
84
|
+
expect(textarea.value).toBe('something else entirely')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('restores over the command text a failed send left in the box', () => {
|
|
88
|
+
// The failure path never runs resetForm(), so the submitted command is
|
|
89
|
+
// still sitting there. Matching it proves the user has not typed since,
|
|
90
|
+
// which makes the box safe to hand the draft back into.
|
|
91
|
+
const { controller, textarea } = buildController()
|
|
92
|
+
stash(controller, 'roadmap notes')
|
|
93
|
+
|
|
94
|
+
textarea.value = '/calendar 2026-08-14'
|
|
95
|
+
controller._restoreStashedDraft('/calendar 2026-08-14')
|
|
96
|
+
|
|
97
|
+
expect(textarea.value).toBe('roadmap notes')
|
|
98
|
+
expect(controller._autoResize).toHaveBeenCalledTimes(1)
|
|
99
|
+
expect(controller._updateSubmitButton).toHaveBeenCalledTimes(1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('does not clobber a review quote the failure path restored', () => {
|
|
103
|
+
// handleSend's .catch reinstates the backed-up quote text before .finally,
|
|
104
|
+
// so the box holds something other than what was submitted.
|
|
105
|
+
const { controller, textarea } = buildController()
|
|
106
|
+
stash(controller, 'roadmap notes')
|
|
107
|
+
|
|
108
|
+
textarea.value = '> quoted review text\n\nmy reply'
|
|
109
|
+
controller._restoreStashedDraft('/calendar 2026-08-14')
|
|
110
|
+
|
|
111
|
+
expect(textarea.value).toBe('> quoted review text\n\nmy reply')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('consumes the stash so a later send does not resurrect it', () => {
|
|
115
|
+
const { controller, textarea } = buildController()
|
|
116
|
+
stash(controller, 'roadmap notes')
|
|
117
|
+
|
|
118
|
+
controller._restoreStashedDraft()
|
|
119
|
+
expect(controller._stashedDraft).toBeNull()
|
|
120
|
+
|
|
121
|
+
textarea.value = ''
|
|
122
|
+
controller._restoreStashedDraft()
|
|
123
|
+
expect(textarea.value).toBe('')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('is a no-op for an ordinary send with nothing stashed', () => {
|
|
127
|
+
const { controller, textarea } = buildController()
|
|
128
|
+
|
|
129
|
+
controller._restoreStashedDraft()
|
|
130
|
+
|
|
131
|
+
expect(textarea.value).toBe('')
|
|
132
|
+
expect(controller._autoResize).not.toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
})
|