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,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
creativeTreeContainer,
|
|
6
|
+
hideTreeEmptyState,
|
|
7
|
+
restoreTreeEmptyState,
|
|
8
|
+
PAGINATION_PENDING_ATTRIBUTE,
|
|
9
|
+
} from '../creative_tree_empty_state'
|
|
10
|
+
|
|
11
|
+
const EMPTY_HTML = '<div data-creatives-empty-state=""><p>No sub-creatives found.</p></div>'
|
|
12
|
+
|
|
13
|
+
const EMPTY_TEMPLATE = `<template id="creatives-empty-state-template">${EMPTY_HTML}</template>`
|
|
14
|
+
|
|
15
|
+
function renderEmptyTree() {
|
|
16
|
+
document.body.innerHTML = `<div id="creatives">${EMPTY_HTML}</div>`
|
|
17
|
+
return document.getElementById('creatives')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The state after a client-side tree render: rows replaced the whole container,
|
|
21
|
+
// so the server-rendered placeholder is gone and only the template survives.
|
|
22
|
+
function renderRenderedTree(innerHtml = '') {
|
|
23
|
+
document.body.innerHTML = `${EMPTY_TEMPLATE}<div id="creatives">${innerHtml}</div>`
|
|
24
|
+
return document.getElementById('creatives')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function placeholder(container) {
|
|
28
|
+
return container.querySelector('[data-creatives-empty-state]')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
document.body.innerHTML = ''
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('creativeTreeContainer resolves the tree container', () => {
|
|
36
|
+
const container = renderEmptyTree()
|
|
37
|
+
expect(creativeTreeContainer()).toBe(container)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('creativeTreeContainer returns null when the tree is not on the page', () => {
|
|
41
|
+
document.body.innerHTML = ''
|
|
42
|
+
expect(creativeTreeContainer()).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('hideTreeEmptyState hides the placeholder', () => {
|
|
46
|
+
const container = renderEmptyTree()
|
|
47
|
+
|
|
48
|
+
hideTreeEmptyState(container)
|
|
49
|
+
|
|
50
|
+
expect(placeholder(container).hidden).toBe(true)
|
|
51
|
+
expect(placeholder(container).style.display).toBe('none')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('hideTreeEmptyState defaults to #creatives when no container is passed', () => {
|
|
55
|
+
const container = renderEmptyTree()
|
|
56
|
+
|
|
57
|
+
hideTreeEmptyState()
|
|
58
|
+
|
|
59
|
+
expect(placeholder(container).hidden).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('hideTreeEmptyState is a no-op without a tree container', () => {
|
|
63
|
+
document.body.innerHTML = ''
|
|
64
|
+
expect(() => hideTreeEmptyState()).not.toThrow()
|
|
65
|
+
expect(() => hideTreeEmptyState(null)).not.toThrow()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('restoreTreeEmptyState shows the placeholder again once the tree is empty', () => {
|
|
69
|
+
const container = renderEmptyTree()
|
|
70
|
+
hideTreeEmptyState(container)
|
|
71
|
+
|
|
72
|
+
restoreTreeEmptyState(container)
|
|
73
|
+
|
|
74
|
+
expect(placeholder(container).hidden).toBe(false)
|
|
75
|
+
expect(placeholder(container).style.display).toBe('')
|
|
76
|
+
expect(placeholder(container).textContent).toContain('No sub-creatives found.')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('restoreTreeEmptyState keeps the placeholder hidden while rows remain', () => {
|
|
80
|
+
const container = renderEmptyTree()
|
|
81
|
+
hideTreeEmptyState(container)
|
|
82
|
+
container.insertAdjacentHTML('beforeend', '<creative-tree-row creative-id="7"></creative-tree-row>')
|
|
83
|
+
|
|
84
|
+
restoreTreeEmptyState(container)
|
|
85
|
+
|
|
86
|
+
expect(placeholder(container).hidden).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('restoreTreeEmptyState does not duplicate the placeholder', () => {
|
|
90
|
+
const container = renderEmptyTree()
|
|
91
|
+
|
|
92
|
+
restoreTreeEmptyState(container)
|
|
93
|
+
|
|
94
|
+
expect(container.querySelectorAll('[data-creatives-empty-state]')).toHaveLength(1)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('restoreTreeEmptyState is a no-op when neither placeholder nor template exists', () => {
|
|
98
|
+
document.body.innerHTML = '<div id="creatives"></div>'
|
|
99
|
+
const container = document.getElementById('creatives')
|
|
100
|
+
|
|
101
|
+
restoreTreeEmptyState(container)
|
|
102
|
+
|
|
103
|
+
expect(container.innerHTML).toBe('')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('restoreTreeEmptyState clones the template when the tree render wiped the container', () => {
|
|
107
|
+
const container = renderRenderedTree()
|
|
108
|
+
|
|
109
|
+
restoreTreeEmptyState(container)
|
|
110
|
+
|
|
111
|
+
const restored = placeholder(container)
|
|
112
|
+
expect(restored).not.toBeNull()
|
|
113
|
+
expect(restored.hidden).toBe(false)
|
|
114
|
+
expect(restored.textContent).toContain('No sub-creatives found.')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('restoreTreeEmptyState does not clone the template while rows remain', () => {
|
|
118
|
+
const container = renderRenderedTree('<creative-tree-row creative-id="7"></creative-tree-row>')
|
|
119
|
+
|
|
120
|
+
restoreTreeEmptyState(container)
|
|
121
|
+
|
|
122
|
+
expect(placeholder(container)).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('restoreTreeEmptyState does not clone the template when a placeholder is already present', () => {
|
|
126
|
+
document.body.innerHTML = `${EMPTY_TEMPLATE}<div id="creatives">${EMPTY_HTML}</div>`
|
|
127
|
+
const container = document.getElementById('creatives')
|
|
128
|
+
hideTreeEmptyState(container)
|
|
129
|
+
|
|
130
|
+
restoreTreeEmptyState(container)
|
|
131
|
+
|
|
132
|
+
expect(container.querySelectorAll('[data-creatives-empty-state]')).toHaveLength(1)
|
|
133
|
+
expect(placeholder(container).hidden).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('cloning the template leaves the template itself untouched', () => {
|
|
137
|
+
const container = renderRenderedTree()
|
|
138
|
+
|
|
139
|
+
restoreTreeEmptyState(container)
|
|
140
|
+
hideTreeEmptyState(container)
|
|
141
|
+
container.replaceChildren()
|
|
142
|
+
restoreTreeEmptyState(container)
|
|
143
|
+
|
|
144
|
+
// A second restore after a second wipe must still produce a visible placeholder:
|
|
145
|
+
// hideTreeEmptyState() must not have mutated the template's own copy.
|
|
146
|
+
expect(placeholder(container).hidden).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('restoreTreeEmptyState is a no-op without a tree container', () => {
|
|
150
|
+
document.body.innerHTML = ''
|
|
151
|
+
expect(() => restoreTreeEmptyState()).not.toThrow()
|
|
152
|
+
expect(() => restoreTreeEmptyState(null)).not.toThrow()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Paginated "Chats" feed: an empty container is not an empty result set while the
|
|
156
|
+
// load-more sentinel still has pages to fetch.
|
|
157
|
+
test('restoreTreeEmptyState stays out of the way while more pages are queued', () => {
|
|
158
|
+
const container = renderRenderedTree()
|
|
159
|
+
container.setAttribute(PAGINATION_PENDING_ATTRIBUTE, 'true')
|
|
160
|
+
|
|
161
|
+
restoreTreeEmptyState(container)
|
|
162
|
+
|
|
163
|
+
expect(placeholder(container)).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('restoreTreeEmptyState leaves an existing placeholder hidden while more pages are queued', () => {
|
|
167
|
+
const container = renderEmptyTree()
|
|
168
|
+
hideTreeEmptyState(container)
|
|
169
|
+
container.setAttribute(PAGINATION_PENDING_ATTRIBUTE, 'true')
|
|
170
|
+
|
|
171
|
+
restoreTreeEmptyState(container)
|
|
172
|
+
|
|
173
|
+
expect(placeholder(container).hidden).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('restoreTreeEmptyState restores once the pending-pages marker is cleared', () => {
|
|
177
|
+
const container = renderRenderedTree()
|
|
178
|
+
container.setAttribute(PAGINATION_PENDING_ATTRIBUTE, 'true')
|
|
179
|
+
restoreTreeEmptyState(container)
|
|
180
|
+
container.removeAttribute(PAGINATION_PENDING_ATTRIBUTE)
|
|
181
|
+
|
|
182
|
+
restoreTreeEmptyState(container)
|
|
183
|
+
|
|
184
|
+
expect(placeholder(container)).not.toBeNull()
|
|
185
|
+
expect(placeholder(container).hidden).toBe(false)
|
|
186
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { jest } from '@jest/globals';
|
|
5
|
+
import { createListenerRegistry } from '../dom_listener_registry';
|
|
6
|
+
|
|
7
|
+
describe('createListenerRegistry', () => {
|
|
8
|
+
test('releases every registered listener', () => {
|
|
9
|
+
const registry = createListenerRegistry();
|
|
10
|
+
const documentHandler = jest.fn();
|
|
11
|
+
const windowHandler = jest.fn();
|
|
12
|
+
registry.add(document, 'registry-document', documentHandler);
|
|
13
|
+
registry.add(window, 'registry-window', windowHandler);
|
|
14
|
+
|
|
15
|
+
registry.releaseAll();
|
|
16
|
+
document.dispatchEvent(new Event('registry-document'));
|
|
17
|
+
window.dispatchEvent(new Event('registry-window'));
|
|
18
|
+
|
|
19
|
+
expect(documentHandler).not.toHaveBeenCalled();
|
|
20
|
+
expect(windowHandler).not.toHaveBeenCalled();
|
|
21
|
+
expect(registry.size).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('rebinds a Turbo session without retaining the old handler', () => {
|
|
25
|
+
const registry = createListenerRegistry();
|
|
26
|
+
const firstSession = jest.fn();
|
|
27
|
+
const secondSession = jest.fn();
|
|
28
|
+
registry.add(document.body, 'click', firstSession);
|
|
29
|
+
|
|
30
|
+
registry.releaseAll();
|
|
31
|
+
registry.add(document.body, 'click', secondSession);
|
|
32
|
+
document.body.click();
|
|
33
|
+
|
|
34
|
+
expect(firstSession).not.toHaveBeenCalled();
|
|
35
|
+
expect(secondSession).toHaveBeenCalledTimes(1);
|
|
36
|
+
registry.releaseAll();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('preserves listener options when removing a binding', () => {
|
|
40
|
+
const registry = createListenerRegistry();
|
|
41
|
+
const handler = jest.fn();
|
|
42
|
+
registry.add(document, 'registry-capture', handler, { capture: true });
|
|
43
|
+
|
|
44
|
+
registry.releaseAll();
|
|
45
|
+
document.dispatchEvent(new Event('registry-capture'));
|
|
46
|
+
|
|
47
|
+
expect(handler).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('ignores targets that cannot register listeners', () => {
|
|
51
|
+
const registry = createListenerRegistry();
|
|
52
|
+
|
|
53
|
+
expect(() => registry.add(null, 'click', jest.fn())).not.toThrow();
|
|
54
|
+
expect(() => registry.add({}, 'click', jest.fn())).not.toThrow();
|
|
55
|
+
expect(registry.size).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('releaseAll is idempotent', () => {
|
|
59
|
+
const registry = createListenerRegistry();
|
|
60
|
+
registry.add(document, 'registry-idempotent', jest.fn());
|
|
61
|
+
|
|
62
|
+
registry.releaseAll();
|
|
63
|
+
|
|
64
|
+
expect(() => registry.releaseAll()).not.toThrow();
|
|
65
|
+
expect(registry.size).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Shared DOM scaffolding for the inline row editor tests.
|
|
2
|
+
//
|
|
3
|
+
// initializeCreativeRowEditor() walks a large, fixed set of element ids that
|
|
4
|
+
// the ERB renders (see _inline_edit_form.html.erb) and bails out on the first
|
|
5
|
+
// missing one, so every suite that drives the real module needs the same
|
|
6
|
+
// skeleton. It lives here rather than in one of the suites so the empty-state
|
|
7
|
+
// recovery and removal tests stay in sync with each other.
|
|
8
|
+
//
|
|
9
|
+
// Not matched by jest's testMatch (`**/__tests__/**/*.test.js`), so it is not
|
|
10
|
+
// collected as a suite of its own.
|
|
11
|
+
|
|
12
|
+
export const BUTTON_IDS = [
|
|
13
|
+
'inline-metadata-btn', 'inline-toggle-markdown', 'inline-move-up', 'inline-move-down',
|
|
14
|
+
'inline-add', 'inline-level-down', 'inline-level-up', 'inline-archive', 'inline-delete',
|
|
15
|
+
'inline-delete-with-children', 'inline-link', 'inline-unconvert', 'inline-unlink',
|
|
16
|
+
'inline-close', 'metadata-popup-close', 'metadata-save-btn',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const HIDDEN_INPUT_IDS = [
|
|
20
|
+
'inline-method', 'inline-creative-description', 'inline-content-type',
|
|
21
|
+
'inline-markdown-editor', 'inline-markdown-source', 'inline-parent-id',
|
|
22
|
+
'inline-before-id', 'inline-after-id', 'inline-child-id', 'inline-origin-id',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export const DIV_IDS = [
|
|
26
|
+
'markdown-editor-wrapper', 'markdown-preview', 'inline-progress-value',
|
|
27
|
+
'inline-save-status', 'metadata-popup',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export function buildEditorDom(container, {
|
|
31
|
+
saveFailedMessage,
|
|
32
|
+
archiveFailedMessage,
|
|
33
|
+
restoreFailedMessage,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const template = document.createElement('div')
|
|
36
|
+
template.id = 'inline-edit-form'
|
|
37
|
+
template.style.display = 'none'
|
|
38
|
+
|
|
39
|
+
const form = document.createElement('form')
|
|
40
|
+
form.id = 'inline-edit-form-element'
|
|
41
|
+
form.action = '/creatives'
|
|
42
|
+
// Mirrors the ERB: plain JS can't call `t()`, so the localized save-failure
|
|
43
|
+
// message rides on the form's data-* attribute.
|
|
44
|
+
if (saveFailedMessage) form.dataset.saveFailedMessage = saveFailedMessage
|
|
45
|
+
template.appendChild(form)
|
|
46
|
+
|
|
47
|
+
HIDDEN_INPUT_IDS.forEach((id) => {
|
|
48
|
+
const input = document.createElement('input')
|
|
49
|
+
input.type = 'hidden'
|
|
50
|
+
input.id = id
|
|
51
|
+
form.appendChild(input)
|
|
52
|
+
})
|
|
53
|
+
const progress = document.createElement('input')
|
|
54
|
+
progress.type = 'checkbox'
|
|
55
|
+
progress.id = 'inline-creative-progress'
|
|
56
|
+
form.appendChild(progress)
|
|
57
|
+
|
|
58
|
+
const editorRoot = document.createElement('div')
|
|
59
|
+
editorRoot.id = 'lexical-inline-editor'
|
|
60
|
+
editorRoot.dataset.lexicalEditorRoot = ''
|
|
61
|
+
form.appendChild(editorRoot)
|
|
62
|
+
|
|
63
|
+
const markdownTextarea = document.createElement('textarea')
|
|
64
|
+
markdownTextarea.id = 'markdown-editor-textarea'
|
|
65
|
+
form.appendChild(markdownTextarea)
|
|
66
|
+
const metadataTextarea = document.createElement('textarea')
|
|
67
|
+
metadataTextarea.id = 'metadata-yaml-editor'
|
|
68
|
+
form.appendChild(metadataTextarea)
|
|
69
|
+
BUTTON_IDS.forEach((id) => {
|
|
70
|
+
const button = document.createElement('button')
|
|
71
|
+
button.type = 'button'
|
|
72
|
+
button.id = id
|
|
73
|
+
if (id === 'inline-archive') {
|
|
74
|
+
// Same reason as saveFailedMessage above: the archive/restore failure
|
|
75
|
+
// strings are localized in the ERB and read back off the button.
|
|
76
|
+
if (archiveFailedMessage) button.dataset.failureMessage = archiveFailedMessage
|
|
77
|
+
if (restoreFailedMessage) button.dataset.restoreFailureMessage = restoreFailedMessage
|
|
78
|
+
}
|
|
79
|
+
form.appendChild(button)
|
|
80
|
+
})
|
|
81
|
+
DIV_IDS.forEach((id) => {
|
|
82
|
+
const div = document.createElement('div')
|
|
83
|
+
div.id = id
|
|
84
|
+
form.appendChild(div)
|
|
85
|
+
})
|
|
86
|
+
container.appendChild(template)
|
|
87
|
+
return template
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The real <creative-tree-row> is a Lit component; the module only needs it to
|
|
91
|
+
// expose a `.creative-tree` child, which is what the browser component renders.
|
|
92
|
+
// A brand-new row has no `.creative-row` yet — that markup only appears after a
|
|
93
|
+
// successful save inserts it — so the stub deliberately omits it.
|
|
94
|
+
export function defineTreeRowStub() {
|
|
95
|
+
if (customElements.get('creative-tree-row')) return
|
|
96
|
+
customElements.define('creative-tree-row', class extends HTMLElement {
|
|
97
|
+
connectedCallback() {
|
|
98
|
+
if (this.querySelector('.creative-tree')) return
|
|
99
|
+
const tree = document.createElement('div')
|
|
100
|
+
tree.className = 'creative-tree'
|
|
101
|
+
this.appendChild(tree)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The markup `_empty_state.html.erb` renders as the sole child of #creatives,
|
|
107
|
+
// reduced to the parts the editor interacts with.
|
|
108
|
+
//
|
|
109
|
+
// The `data-creatives-empty-state` wrapper is what makes this a placeholder as far
|
|
110
|
+
// as `creative_tree_empty_state.js` is concerned — that module hides and restores
|
|
111
|
+
// the wrapper, not the card inside it — so the fixture has to carry it or the
|
|
112
|
+
// hide/restore calls silently find nothing. `index.html.erb` also emits a pristine
|
|
113
|
+
// copy in a `<template>` outside the container, which `restoreTreeEmptyState()`
|
|
114
|
+
// clones when the tree render has wiped the original away; mirror that here so the
|
|
115
|
+
// restore path is exercised as it is in the browser.
|
|
116
|
+
export function renderEmptyState() {
|
|
117
|
+
const creatives = document.getElementById('creatives')
|
|
118
|
+
const markup = `
|
|
119
|
+
<div data-creatives-empty-state>
|
|
120
|
+
<div class="creative-empty-state">
|
|
121
|
+
<button type="button" class="new-root-creative-btn">Add</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
`
|
|
125
|
+
creatives.innerHTML = markup
|
|
126
|
+
|
|
127
|
+
document.getElementById('creatives-empty-state-template')?.remove()
|
|
128
|
+
const template = document.createElement('template')
|
|
129
|
+
template.id = 'creatives-empty-state-template'
|
|
130
|
+
template.innerHTML = markup
|
|
131
|
+
document.body.appendChild(template)
|
|
132
|
+
|
|
133
|
+
return creatives.querySelector('[data-creatives-empty-state]')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A row that already exists on the server, i.e. one that renders a
|
|
137
|
+
// `.creative-row` and carries a creative id.
|
|
138
|
+
export function appendExistingRow(id, container = document.getElementById('creatives')) {
|
|
139
|
+
const rowComponent = document.createElement('creative-tree-row')
|
|
140
|
+
rowComponent.setAttribute('creative-id', String(id))
|
|
141
|
+
rowComponent.dataset.descriptionRawHtml = '<p>existing</p>'
|
|
142
|
+
rowComponent.dataset.progressValue = '0'
|
|
143
|
+
// The stub only renders its .creative-tree child once connected.
|
|
144
|
+
container.appendChild(rowComponent)
|
|
145
|
+
const tree = rowComponent.querySelector('.creative-tree')
|
|
146
|
+
tree.id = `creative-${id}`
|
|
147
|
+
tree.dataset.id = String(id)
|
|
148
|
+
const row = document.createElement('div')
|
|
149
|
+
row.className = 'creative-row'
|
|
150
|
+
tree.appendChild(row)
|
|
151
|
+
return { rowComponent, tree, row }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Lets queued microtasks and the rAF-deferred finalizeSetup() in startNew() run.
|
|
155
|
+
export function flush() {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
requestAnimationFrame(() => setTimeout(resolve, 0))
|
|
158
|
+
})
|
|
159
|
+
}
|
|
@@ -348,9 +348,6 @@ export default class CommandArgsForm {
|
|
|
348
348
|
if (command.name === 'topic') {
|
|
349
349
|
return this._buildTopicText(command, values)
|
|
350
350
|
}
|
|
351
|
-
if (command.name === 'work') {
|
|
352
|
-
return this._buildWorkText(command, values)
|
|
353
|
-
}
|
|
354
351
|
if (command.name === 'compress') {
|
|
355
352
|
return this._buildCompressText(command, values)
|
|
356
353
|
}
|
|
@@ -375,15 +372,6 @@ export default class CommandArgsForm {
|
|
|
375
372
|
return parts.join(' ')
|
|
376
373
|
}
|
|
377
374
|
|
|
378
|
-
_buildWorkText(command, values) {
|
|
379
|
-
const parts = [command.label]
|
|
380
|
-
if (values.agent_name) {
|
|
381
|
-
parts.push(values.agent_name)
|
|
382
|
-
}
|
|
383
|
-
if (values.context) parts.push(values.context)
|
|
384
|
-
return parts.join(' ')
|
|
385
|
-
}
|
|
386
|
-
|
|
387
375
|
_buildCompressText(command, values) {
|
|
388
376
|
const parts = [command.label]
|
|
389
377
|
if (values.instructions) parts.push(values.instructions)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import CommonPopup from '../lib/common_popup'
|
|
2
|
-
import {
|
|
2
|
+
import { caretAnchor } from '../utils/caret_position'
|
|
3
3
|
import CommandArgsForm from './command_args_form'
|
|
4
|
+
import { openCreativeLinkPicker } from './creative_link_picker'
|
|
4
5
|
|
|
5
6
|
let commandMenuInitialized = false
|
|
7
|
+
let commandSubmissionSequence = 0
|
|
6
8
|
|
|
7
9
|
if (!commandMenuInitialized) {
|
|
8
10
|
commandMenuInitialized = true
|
|
@@ -15,6 +17,7 @@ if (!commandMenuInitialized) {
|
|
|
15
17
|
|
|
16
18
|
const list = menu.querySelector('[data-popup-list]')
|
|
17
19
|
const commandCache = new Map()
|
|
20
|
+
let skipNextMenuInput = false
|
|
18
21
|
|
|
19
22
|
const argsForm = new CommandArgsForm({
|
|
20
23
|
container: popup,
|
|
@@ -33,12 +36,53 @@ if (!commandMenuInitialized) {
|
|
|
33
36
|
},
|
|
34
37
|
onSubmit: (commandText) => {
|
|
35
38
|
popupMenu.hide()
|
|
39
|
+
// clearCommandText() stripped only the "/" token, so anything still in
|
|
40
|
+
// the box is the draft the user had typed before opening the menu. The
|
|
41
|
+
// command is submitted on its own, so the draft cannot ride along with
|
|
42
|
+
// it — hand it to the form controller, which puts it back once the send
|
|
43
|
+
// settles, rather than dropping it on the floor here.
|
|
44
|
+
const draft = textarea.value
|
|
45
|
+
if (draft.trim()) {
|
|
46
|
+
textarea.dispatchEvent(new CustomEvent('comments--form:stash-draft', {
|
|
47
|
+
bubbles: true,
|
|
48
|
+
detail: { draft }
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
36
51
|
textarea.value = commandText
|
|
52
|
+
// Keep the normal input listeners (drafts, resize, submit state) in
|
|
53
|
+
// sync without treating this programmatic command as a new typeahead
|
|
54
|
+
// query. A schema command with no entered values is just "/name", so
|
|
55
|
+
// the command menu would otherwise reopen as soon as the form closes.
|
|
56
|
+
skipNextMenuInput = true
|
|
37
57
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
38
58
|
// Trigger form submission directly
|
|
39
59
|
const submitBtn = document.querySelector('#new-comment-form [data-comments--form-target="submit"]')
|
|
40
60
|
if (submitBtn) {
|
|
61
|
+
const submissionId = String(++commandSubmissionSequence)
|
|
62
|
+
let submissionStarted = false
|
|
63
|
+
const markSubmission = (event) => { event.commandSubmissionId = submissionId }
|
|
64
|
+
const onSubmissionStarted = (event) => {
|
|
65
|
+
if (event.detail?.submissionId === submissionId) submissionStarted = true
|
|
66
|
+
}
|
|
67
|
+
// Keep focus out of the textarea while its value is still the command
|
|
68
|
+
// being sent. The form announces settlement after success/failure
|
|
69
|
+
// cleanup, so typing cannot append to the in-flight command.
|
|
70
|
+
const onSubmissionSettled = (event) => {
|
|
71
|
+
if (event.detail?.submissionId !== submissionId) return
|
|
72
|
+
popup.removeEventListener('comments--form:submit-settled', onSubmissionSettled)
|
|
73
|
+
const active = document.activeElement
|
|
74
|
+
if (!active || active === document.body) textarea.focus()
|
|
75
|
+
}
|
|
76
|
+
submitBtn.addEventListener('click', markSubmission, { capture: true, once: true })
|
|
77
|
+
popup.addEventListener('comments--form:submit-started', onSubmissionStarted)
|
|
78
|
+
popup.addEventListener('comments--form:submit-settled', onSubmissionSettled)
|
|
41
79
|
submitBtn.click()
|
|
80
|
+
submitBtn.removeEventListener('click', markSubmission, true)
|
|
81
|
+
popup.removeEventListener('comments--form:submit-started', onSubmissionStarted)
|
|
82
|
+
if (!submissionStarted) {
|
|
83
|
+
popup.removeEventListener('comments--form:submit-settled', onSubmissionSettled)
|
|
84
|
+
textarea.focus()
|
|
85
|
+
}
|
|
42
86
|
}
|
|
43
87
|
},
|
|
44
88
|
onCancel: () => {
|
|
@@ -115,18 +159,25 @@ if (!commandMenuInitialized) {
|
|
|
115
159
|
textarea.setSelectionRange(cleaned.length, cleaned.length)
|
|
116
160
|
}
|
|
117
161
|
|
|
162
|
+
// Cached by creative id, and cached as the *promise* rather than its result:
|
|
163
|
+
// the list is fetched on every keystroke, so caching only after the response
|
|
164
|
+
// lands would fire one request per character of "/task" before the first one
|
|
165
|
+
// returns. In-flight callers share the request instead.
|
|
118
166
|
function fetchCommands(creativeId) {
|
|
119
167
|
if (!creativeId) return Promise.resolve([])
|
|
120
|
-
if (commandCache.has(creativeId)) return
|
|
168
|
+
if (commandCache.has(creativeId)) return commandCache.get(creativeId)
|
|
121
169
|
|
|
122
|
-
|
|
170
|
+
const request = fetch(`/creatives/${creativeId}/comments/commands`, { headers: { Accept: 'application/json' } })
|
|
123
171
|
.then((response) => (response.ok ? response.json() : []))
|
|
124
|
-
.then((data) =>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
172
|
+
.then((data) => (Array.isArray(data) ? data : []))
|
|
173
|
+
.catch(() => {
|
|
174
|
+
// Don't leave a failed lookup cached — the next keystroke retries.
|
|
175
|
+
commandCache.delete(creativeId)
|
|
176
|
+
return []
|
|
128
177
|
})
|
|
129
|
-
|
|
178
|
+
|
|
179
|
+
commandCache.set(creativeId, request)
|
|
180
|
+
return request
|
|
130
181
|
}
|
|
131
182
|
|
|
132
183
|
function insert(command) {
|
|
@@ -158,45 +209,33 @@ if (!commandMenuInitialized) {
|
|
|
158
209
|
}
|
|
159
210
|
|
|
160
211
|
popupMenu.setItems(filtered)
|
|
161
|
-
|
|
162
|
-
popupMenu.showAt(caretRect)
|
|
212
|
+
popupMenu.showAt(caretAnchor(textarea))
|
|
163
213
|
}
|
|
164
214
|
|
|
165
215
|
function openCreativePicker(textarea) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const controller = window.Stimulus?.getControllerForElementAndIdentifier(modal, 'link-creative')
|
|
170
|
-
if (!controller) return
|
|
171
|
-
|
|
172
|
-
// Clear the command text (e.g. "/crea", "/creative") from textarea
|
|
173
|
-
clearCommandText()
|
|
174
|
-
|
|
175
|
-
const caretRect = getCaretClientRect(textarea) || textarea.getBoundingClientRect()
|
|
176
|
-
controller.open(
|
|
177
|
-
caretRect,
|
|
178
|
-
(item) => {
|
|
179
|
-
// Insert markdown link at cursor position
|
|
180
|
-
const link = `[${item.label}](/creatives/${item.id})`
|
|
181
|
-
const curPos = textarea.selectionStart
|
|
182
|
-
const beforeCur = textarea.value.slice(0, curPos)
|
|
183
|
-
const afterCur = textarea.value.slice(curPos)
|
|
184
|
-
textarea.value = beforeCur + link + ' ' + afterCur
|
|
185
|
-
const newPos = curPos + link.length + 1
|
|
186
|
-
textarea.setSelectionRange(newPos, newPos)
|
|
187
|
-
textarea.focus()
|
|
188
|
-
},
|
|
189
|
-
() => {
|
|
190
|
-
textarea.focus()
|
|
191
|
-
}
|
|
192
|
-
)
|
|
216
|
+
openCreativeLinkPicker(textarea, {
|
|
217
|
+
triggerRange: { start: 0, end: textarea.selectionStart },
|
|
218
|
+
})
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
textarea.addEventListener('keydown', function (event) {
|
|
196
222
|
if (popupMenu.handleKey(event)) return
|
|
197
223
|
})
|
|
198
224
|
|
|
225
|
+
// Bumped by every input event, so a lookup that resolves after the user has
|
|
226
|
+
// typed on (or deleted the "/" entirely) is dropped instead of rendering the
|
|
227
|
+
// menu from a query that no longer matches the box — the late response would
|
|
228
|
+
// otherwise pop the menu back up over an unrelated draft.
|
|
229
|
+
let queryToken = 0
|
|
230
|
+
|
|
199
231
|
textarea.addEventListener('input', function () {
|
|
232
|
+
const token = ++queryToken
|
|
233
|
+
if (skipNextMenuInput) {
|
|
234
|
+
skipNextMenuInput = false
|
|
235
|
+
popupMenu.hide()
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
200
239
|
// If args form is open, don't show command menu
|
|
201
240
|
if (argsForm.isOpen()) return
|
|
202
241
|
|
|
@@ -213,7 +252,10 @@ if (!commandMenuInitialized) {
|
|
|
213
252
|
const query = match[1]
|
|
214
253
|
|
|
215
254
|
fetchCommands(creativeId)
|
|
216
|
-
.then((commands) =>
|
|
255
|
+
.then((commands) => {
|
|
256
|
+
if (token !== queryToken) return
|
|
257
|
+
show(commands, query)
|
|
258
|
+
})
|
|
217
259
|
})
|
|
218
260
|
})
|
|
219
261
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { caretAnchor } from '../utils/caret_position'
|
|
2
|
+
|
|
3
|
+
const MARKDOWN_SPECIAL = /[\\`*_\[\]()<>~]/g
|
|
4
|
+
|
|
5
|
+
export function escapeMarkdownLabel(label) {
|
|
6
|
+
return String(label || '').replace(MARKDOWN_SPECIAL, (character) => `\\${character}`)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function markdownCreativeCommandRange(value, caretPosition) {
|
|
10
|
+
const beforeCaret = String(value || '').slice(0, caretPosition)
|
|
11
|
+
const match = beforeCaret.match(/(?:^|\n)(\/creative)$/)
|
|
12
|
+
if (!match) return null
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
start: caretPosition - match[1].length,
|
|
16
|
+
end: caretPosition,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function insertMarkdownCreativeLink(textarea, item) {
|
|
21
|
+
if (!textarea || !item?.id) return false
|
|
22
|
+
|
|
23
|
+
const position = textarea.selectionStart
|
|
24
|
+
const link = `[${escapeMarkdownLabel(item.label)}](/creatives/${item.id})`
|
|
25
|
+
textarea.setRangeText(`${link} `, position, textarea.selectionEnd, 'end')
|
|
26
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
27
|
+
textarea.focus()
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function openCreativeLinkPicker(textarea, {
|
|
32
|
+
triggerRange = null,
|
|
33
|
+
allowCreate = true,
|
|
34
|
+
} = {}) {
|
|
35
|
+
if (!textarea) return false
|
|
36
|
+
|
|
37
|
+
const modal = document.getElementById('link-creative-modal')
|
|
38
|
+
const controller = modal && window.Stimulus?.getControllerForElementAndIdentifier(
|
|
39
|
+
modal,
|
|
40
|
+
'link-creative'
|
|
41
|
+
)
|
|
42
|
+
if (!controller) return false
|
|
43
|
+
|
|
44
|
+
if (triggerRange) {
|
|
45
|
+
textarea.setRangeText('', triggerRange.start, triggerRange.end, 'end')
|
|
46
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
controller.open(
|
|
50
|
+
caretAnchor(textarea),
|
|
51
|
+
(item) => insertMarkdownCreativeLink(textarea, item),
|
|
52
|
+
() => textarea.focus(),
|
|
53
|
+
{ allowCreate }
|
|
54
|
+
)
|
|
55
|
+
return true
|
|
56
|
+
}
|