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
|
@@ -5,7 +5,15 @@ import csrfFetch from '../../lib/api/csrf_fetch'
|
|
|
5
5
|
import { alertDialog } from '../../lib/utils/dialog'
|
|
6
6
|
|
|
7
7
|
const TYPING_TIMEOUT = 3000
|
|
8
|
-
const
|
|
8
|
+
const AGENT_TASK_POLL_INTERVAL = 15000 // Poll active task statuses every 15s
|
|
9
|
+
const STREAMING_HEARTBEAT_TIMEOUT = 5000 // Transition streaming → thinking if no heartbeat
|
|
10
|
+
const PRESENCE_HEARTBEAT_INTERVAL = 30000
|
|
11
|
+
|
|
12
|
+
// agent_status values that keep a task registered. thinking/streaming are the
|
|
13
|
+
// agent producing output; pending_approval is it paused on a tool approval,
|
|
14
|
+
// which is not the end of the task — treating it as idle would drop the task
|
|
15
|
+
// and stop the poll, so the status above could never be seen.
|
|
16
|
+
const LIVE_AGENT_STATUSES = new Set(['thinking', 'streaming', 'pending_approval'])
|
|
9
17
|
|
|
10
18
|
export default class extends Controller {
|
|
11
19
|
static targets = ['participants', 'typingIndicator', 'textarea', 'privateCheckbox', 'channelChips', 'scrollRow']
|
|
@@ -19,31 +27,66 @@ export default class extends Controller {
|
|
|
19
27
|
this.manualTypingMessage = null
|
|
20
28
|
this.presenceSubscription = null
|
|
21
29
|
this.typingTimeoutHandle = null
|
|
30
|
+
this.activeAgentTasks = {} // { agentId: [taskId, ...] } - ordered, last is most recent
|
|
31
|
+
this.agentStates = {} // { agentId: 'streaming' | 'thinking' }
|
|
32
|
+
this.streamingHeartbeatTimers = {} // { agentId: timeoutHandle }
|
|
33
|
+
this.agentTaskPollHandle = null
|
|
22
34
|
this.hasPresenceConnected = false
|
|
35
|
+
this.presenceHeartbeatHandle = null
|
|
23
36
|
this.currentUserId = document.body.dataset.currentUserId
|
|
37
|
+
this.selectedTopicId = null
|
|
38
|
+
this.mainTopicId = null
|
|
39
|
+
this.renderedAllTopicIds = null
|
|
40
|
+
this.renderedAllIncludesLegacy = false
|
|
24
41
|
|
|
25
42
|
this.handleInput = this.handleInput.bind(this)
|
|
26
43
|
this.handleFocus = this.handleFocus.bind(this)
|
|
27
44
|
this.handleBlur = this.handleBlur.bind(this)
|
|
28
45
|
this.handleTopicChange = this.handleTopicChange.bind(this)
|
|
46
|
+
this.handleRenderedAllTopics = this.handleRenderedAllTopics.bind(this)
|
|
29
47
|
|
|
30
48
|
this.textareaTarget.addEventListener('input', this.handleInput)
|
|
31
49
|
this.textareaTarget.addEventListener('focus', this.handleFocus)
|
|
32
50
|
this.textareaTarget.addEventListener('blur', this.handleBlur)
|
|
33
51
|
this.privateCheckboxTarget?.addEventListener('change', () => this.stoppedTyping())
|
|
34
52
|
this.element.addEventListener('comments--topics:change', this.handleTopicChange)
|
|
53
|
+
this.element.addEventListener('comments--list:rendered-all-topics', this.handleRenderedAllTopics)
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
disconnect() {
|
|
38
57
|
this.unsubscribe()
|
|
58
|
+
this.stopAgentTaskPoll()
|
|
59
|
+
this.clearAllStreamingHeartbeats()
|
|
39
60
|
this.textareaTarget.removeEventListener('input', this.handleInput)
|
|
40
61
|
this.textareaTarget.removeEventListener('focus', this.handleFocus)
|
|
41
62
|
this.textareaTarget.removeEventListener('blur', this.handleBlur)
|
|
42
63
|
this.element.removeEventListener('comments--topics:change', this.handleTopicChange)
|
|
64
|
+
this.element.removeEventListener('comments--list:rendered-all-topics', this.handleRenderedAllTopics)
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
handleTopicChange(event) {
|
|
46
68
|
const topicId = event.detail?.topicId
|
|
69
|
+
const nextSelectedTopicId = topicId || null
|
|
70
|
+
const nextMainTopicId = event.detail?.mainTopicId || this.mainTopicId
|
|
71
|
+
const selectionChanged = String(nextSelectedTopicId || '') !== String(this.selectedTopicId || '')
|
|
72
|
+
|
|
73
|
+
if (selectionChanged) {
|
|
74
|
+
this.stoppedTyping()
|
|
75
|
+
this.clearTopicIndicators()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.selectedTopicId = nextSelectedTopicId
|
|
79
|
+
this.mainTopicId = nextMainTopicId
|
|
80
|
+
if (selectionChanged) {
|
|
81
|
+
this.renderedAllTopicIds = null
|
|
82
|
+
this.renderedAllIncludesLegacy = false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (selectionChanged) {
|
|
86
|
+
this.reportViewingTopic()
|
|
87
|
+
this.requestRunningAgents()
|
|
88
|
+
}
|
|
89
|
+
|
|
47
90
|
if (topicId) {
|
|
48
91
|
this.refreshChannelChips(topicId)
|
|
49
92
|
} else {
|
|
@@ -51,6 +94,15 @@ export default class extends Controller {
|
|
|
51
94
|
}
|
|
52
95
|
}
|
|
53
96
|
|
|
97
|
+
handleRenderedAllTopics(event) {
|
|
98
|
+
const { creativeId, topicIds, includesLegacy } = event.detail || {}
|
|
99
|
+
if (String(creativeId) !== String(this.creativeId) || this.selectedTopicId) return
|
|
100
|
+
|
|
101
|
+
this.renderedAllTopicIds = Array.isArray(topicIds) ? topicIds : []
|
|
102
|
+
this.renderedAllIncludesLegacy = Boolean(includesLegacy)
|
|
103
|
+
this.reportViewingTopic()
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
get listController() {
|
|
55
107
|
return this.application.getControllerForElementAndIdentifier(this.element, 'comments--list')
|
|
56
108
|
}
|
|
@@ -64,7 +116,18 @@ export default class extends Controller {
|
|
|
64
116
|
}
|
|
65
117
|
|
|
66
118
|
onPopupOpened({ creativeId }) {
|
|
119
|
+
// Navigating the OPEN popup to another creative comes through here, not through
|
|
120
|
+
// onPopupClosed() — PopupController#_navigateToEntry reuses open()/openForCreative().
|
|
121
|
+
// Every piece of agent state below belongs to the chat being left: the poll is keyed
|
|
122
|
+
// on task ids and /tasks/active_statuses answers for any task the user can read, so a
|
|
123
|
+
// carried-over id keeps a foreign task's indicator alive here, and the Stop button it
|
|
124
|
+
// renders cancels a turn in a creative that is no longer on screen.
|
|
125
|
+
if (this.creativeId !== undefined && String(creativeId) !== String(this.creativeId)) {
|
|
126
|
+
this.resetAgentActivity()
|
|
127
|
+
}
|
|
67
128
|
this.creativeId = creativeId
|
|
129
|
+
this.renderedAllTopicIds = null
|
|
130
|
+
this.renderedAllIncludesLegacy = false
|
|
68
131
|
this.loadParticipants()
|
|
69
132
|
this.subscribe()
|
|
70
133
|
this.renderParticipants([])
|
|
@@ -81,6 +144,9 @@ export default class extends Controller {
|
|
|
81
144
|
this.element, 'comments--topics'
|
|
82
145
|
)
|
|
83
146
|
const topicId = topicsCtrl?.currentTopicId
|
|
147
|
+
this.selectedTopicId = topicId || null
|
|
148
|
+
this.mainTopicId = topicsCtrl?.mainTopicId || null
|
|
149
|
+
this.reportViewingTopic()
|
|
84
150
|
if (topicId) {
|
|
85
151
|
this.refreshChannelChips(topicId)
|
|
86
152
|
} else {
|
|
@@ -88,14 +154,50 @@ export default class extends Controller {
|
|
|
88
154
|
}
|
|
89
155
|
}
|
|
90
156
|
|
|
157
|
+
// Everything that describes activity in ONE chat. Leaving that chat — closed, or
|
|
158
|
+
// navigated to another creative with the popup still open — has to drop all of it
|
|
159
|
+
// together: the maps the indicator renders from, the timers that write back into
|
|
160
|
+
// them, and the poll that keeps them alive. Clearing the maps alone would leave the
|
|
161
|
+
// interval and the heartbeat timeouts running for a chat nobody is looking at.
|
|
162
|
+
resetAgentActivity() {
|
|
163
|
+
this.stopAgentTaskPoll()
|
|
164
|
+
this.clearAllStreamingHeartbeats()
|
|
165
|
+
this.clearTypingTimers()
|
|
166
|
+
this.typingUsers = {}
|
|
167
|
+
this.activeAgentTasks = {}
|
|
168
|
+
this.agentStates = {}
|
|
169
|
+
this.syncGlobalAgentTasks()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// An agent's turns, label, state and heartbeat come down together or not at all.
|
|
173
|
+
// The heartbeat is keyed by agent, so it may only go when the agent itself does —
|
|
174
|
+
// a surviving turn still needs it to degrade to thinking. Three paths remove an
|
|
175
|
+
// agent (idle, Stop, poll); routing them all through here is what keeps them from
|
|
176
|
+
// drifting apart, which is how that heartbeat got dropped early once already.
|
|
177
|
+
dropAgent(agentId) {
|
|
178
|
+
delete this.activeAgentTasks[agentId]
|
|
179
|
+
delete this.typingUsers[agentId]
|
|
180
|
+
delete this.agentStates[agentId]
|
|
181
|
+
this.clearStreamingHeartbeat(agentId)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Ends one turn, and the agent with it once nothing is left. A missing taskId means
|
|
185
|
+
// the caller can't say which turn ended, so the agent goes entirely.
|
|
186
|
+
dropAgentTask(agentId, taskId) {
|
|
187
|
+
const tasks = this.activeAgentTasks[agentId]
|
|
188
|
+
if (!tasks) return this.dropAgent(agentId)
|
|
189
|
+
if (taskId) {
|
|
190
|
+
const idx = tasks.indexOf(taskId)
|
|
191
|
+
if (idx !== -1) tasks.splice(idx, 1)
|
|
192
|
+
}
|
|
193
|
+
if (!taskId || tasks.length === 0) this.dropAgent(agentId)
|
|
194
|
+
}
|
|
195
|
+
|
|
91
196
|
onPopupClosed() {
|
|
92
197
|
this.unsubscribe()
|
|
93
198
|
this.participantsData = null
|
|
94
199
|
this.currentPresentIds = []
|
|
95
|
-
this.
|
|
96
|
-
this.activeAgentTasks = {}
|
|
97
|
-
this.syncGlobalAgentTasks()
|
|
98
|
-
this.clearTypingTimers()
|
|
200
|
+
this.resetAgentActivity()
|
|
99
201
|
this.clearManualTypingMessage()
|
|
100
202
|
this.renderParticipants([])
|
|
101
203
|
this.renderTypingIndicator()
|
|
@@ -116,13 +218,17 @@ export default class extends Controller {
|
|
|
116
218
|
|
|
117
219
|
typing() {
|
|
118
220
|
if (!this.presenceSubscription || this.privateCheckboxTarget?.checked) return
|
|
119
|
-
this.
|
|
221
|
+
const topicId = this.typingTopicId
|
|
222
|
+
if (!topicId) return
|
|
223
|
+
|
|
224
|
+
this.presenceSubscription.perform('typing', { topic_id: topicId })
|
|
120
225
|
this.resetTypingTimeout()
|
|
121
226
|
}
|
|
122
227
|
|
|
123
228
|
stoppedTyping() {
|
|
124
|
-
|
|
125
|
-
|
|
229
|
+
const topicId = this.typingTopicId
|
|
230
|
+
if (this.presenceSubscription && topicId) {
|
|
231
|
+
this.presenceSubscription.perform('stopped_typing', { topic_id: topicId })
|
|
126
232
|
}
|
|
127
233
|
if (this.typingTimeoutHandle) {
|
|
128
234
|
clearTimeout(this.typingTimeoutHandle)
|
|
@@ -130,7 +236,13 @@ export default class extends Controller {
|
|
|
130
236
|
}
|
|
131
237
|
}
|
|
132
238
|
|
|
133
|
-
|
|
239
|
+
requestRunningAgents() {
|
|
240
|
+
if (!this.presenceSubscription || !this.selectedTopicId) return
|
|
241
|
+
|
|
242
|
+
this.presenceSubscription.perform('running_agents', { topic_id: this.selectedTopicId })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
loadParticipants() {
|
|
134
246
|
if (!this.creativeId) return
|
|
135
247
|
fetch(`/creatives/${this.creativeId}/comments/participants`, {
|
|
136
248
|
cache: 'no-store',
|
|
@@ -150,16 +262,11 @@ export default class extends Controller {
|
|
|
150
262
|
this.renderParticipants(this.currentPresentIds)
|
|
151
263
|
this.renderTypingIndicator()
|
|
152
264
|
})
|
|
153
|
-
.catch((
|
|
265
|
+
.catch(() => {
|
|
154
266
|
this.participantsData = []
|
|
155
267
|
this.canShare = false
|
|
156
268
|
this.renderParticipants([])
|
|
157
269
|
this.renderTypingIndicator()
|
|
158
|
-
|
|
159
|
-
if (closeOnForbidden) {
|
|
160
|
-
alertDialog(error.message)
|
|
161
|
-
this.popupController?.close()
|
|
162
|
-
}
|
|
163
270
|
})
|
|
164
271
|
}
|
|
165
272
|
|
|
@@ -175,6 +282,8 @@ export default class extends Controller {
|
|
|
175
282
|
this.listController?.loadInitialComments()
|
|
176
283
|
}
|
|
177
284
|
this.hasPresenceConnected = true
|
|
285
|
+
this.reportViewingTopic()
|
|
286
|
+
this.startPresenceHeartbeat()
|
|
178
287
|
},
|
|
179
288
|
received: (data) => this.handlePresenceMessage(data),
|
|
180
289
|
},
|
|
@@ -182,6 +291,7 @@ export default class extends Controller {
|
|
|
182
291
|
}
|
|
183
292
|
|
|
184
293
|
unsubscribe() {
|
|
294
|
+
this.stopPresenceHeartbeat()
|
|
185
295
|
if (this.presenceSubscription) {
|
|
186
296
|
this.presenceSubscription.unsubscribe()
|
|
187
297
|
this.presenceSubscription = null
|
|
@@ -189,6 +299,31 @@ export default class extends Controller {
|
|
|
189
299
|
this.stoppedTyping()
|
|
190
300
|
}
|
|
191
301
|
|
|
302
|
+
reportViewingTopic() {
|
|
303
|
+
if (!this.presenceSubscription) return
|
|
304
|
+
|
|
305
|
+
const payload = { topic_id: this.selectedTopicId }
|
|
306
|
+
if (!this.selectedTopicId && Array.isArray(this.renderedAllTopicIds)) {
|
|
307
|
+
payload.rendered_topic_ids = this.renderedAllTopicIds
|
|
308
|
+
if (this.renderedAllIncludesLegacy) payload.rendered_legacy_topic = true
|
|
309
|
+
}
|
|
310
|
+
this.presenceSubscription.perform('viewing_topic', payload)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
startPresenceHeartbeat() {
|
|
314
|
+
this.stopPresenceHeartbeat()
|
|
315
|
+
this.presenceHeartbeatHandle = setInterval(() => {
|
|
316
|
+
this.presenceSubscription?.perform('heartbeat')
|
|
317
|
+
}, PRESENCE_HEARTBEAT_INTERVAL)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
stopPresenceHeartbeat() {
|
|
321
|
+
if (this.presenceHeartbeatHandle) {
|
|
322
|
+
clearInterval(this.presenceHeartbeatHandle)
|
|
323
|
+
this.presenceHeartbeatHandle = null
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
192
327
|
handlePresenceMessage(data) {
|
|
193
328
|
if (data.ids) {
|
|
194
329
|
this.currentPresentIds = data.ids.map((id) => parseInt(id, 10))
|
|
@@ -196,7 +331,9 @@ export default class extends Controller {
|
|
|
196
331
|
this.updateReadReceiptPresence(this.currentPresentIds)
|
|
197
332
|
}
|
|
198
333
|
if (data.typing) {
|
|
199
|
-
const { id, name } = data.typing
|
|
334
|
+
const { id, name, topic_id: topicId } = data.typing
|
|
335
|
+
if (!this.isSelectedTopic(topicId)) return
|
|
336
|
+
|
|
200
337
|
const isNewTyper = !(id in this.typingUsers)
|
|
201
338
|
// The user always wants to see their OWN typing indicator the moment it
|
|
202
339
|
// appears — they just started typing. Stick-to-end (which pauses while the
|
|
@@ -213,7 +350,9 @@ export default class extends Controller {
|
|
|
213
350
|
}, TYPING_TIMEOUT)
|
|
214
351
|
}
|
|
215
352
|
if (data.stop_typing) {
|
|
216
|
-
const { id } = data.stop_typing
|
|
353
|
+
const { id, topic_id: topicId } = data.stop_typing
|
|
354
|
+
if (!this.isSelectedTopic(topicId)) return
|
|
355
|
+
|
|
217
356
|
delete this.typingUsers[id]
|
|
218
357
|
if (this.typingTimers[id]) {
|
|
219
358
|
clearTimeout(this.typingTimers[id])
|
|
@@ -234,40 +373,63 @@ export default class extends Controller {
|
|
|
234
373
|
this.formController?.setCommentPermission(shareChange.can_comment)
|
|
235
374
|
}
|
|
236
375
|
|
|
237
|
-
|
|
376
|
+
if (shareChange.has_access === false) {
|
|
377
|
+
document.dispatchEvent(new CustomEvent('workspace-tree:invalidate', {
|
|
378
|
+
detail: { creativeIds: [String(this.creativeId)] },
|
|
379
|
+
}))
|
|
380
|
+
alertDialog(this.element.dataset.noPermissionText || 'No permission')
|
|
381
|
+
if (this.popupController?.isDocked()) {
|
|
382
|
+
this.popupController.resetDockedToEmpty()
|
|
383
|
+
} else {
|
|
384
|
+
this.popupController?.close()
|
|
385
|
+
}
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.loadParticipants()
|
|
238
390
|
return
|
|
239
391
|
}
|
|
240
392
|
if (data.channel_chips) {
|
|
241
393
|
this.refreshChannelChips(data.channel_chips.topic_id)
|
|
242
394
|
}
|
|
243
395
|
if (data.agent_status) {
|
|
244
|
-
const { id, name, status, task_id, creative_id: agentCreativeId } = data.agent_status
|
|
396
|
+
const { id, name, status, task_id, topic_id: topicId, creative_id: agentCreativeId } = data.agent_status
|
|
245
397
|
// Only show typing indicator if agent is working on this specific creative
|
|
246
398
|
if (agentCreativeId && String(agentCreativeId) !== String(this.creativeId)) {
|
|
247
399
|
return
|
|
248
400
|
}
|
|
401
|
+
if (!topicId) {
|
|
402
|
+
// Turns are tracked per agent as an array, so a legacy idle has to match a
|
|
403
|
+
// member — String() on the array only happens to work at length 1.
|
|
404
|
+
const activeTaskIds = this.activeAgentTasks?.[id] || []
|
|
405
|
+
const matchingLegacyIdle = status === 'idle' && task_id &&
|
|
406
|
+
activeTaskIds.some((activeTaskId) => String(activeTaskId) === String(task_id))
|
|
407
|
+
if (!matchingLegacyIdle) return
|
|
408
|
+
} else if (!this.isSelectedTopic(topicId)) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
249
412
|
const isNewAgent = (status === 'thinking' || status === 'streaming') && !(id in this.typingUsers)
|
|
250
|
-
if (status
|
|
413
|
+
if (LIVE_AGENT_STATUSES.has(status)) {
|
|
251
414
|
this.typingUsers[id] = name
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (this.agentStatusTimers[id]) clearTimeout(this.agentStatusTimers[id])
|
|
257
|
-
this.agentStatusTimers[id] = setTimeout(() => {
|
|
258
|
-
delete this.typingUsers[id]
|
|
259
|
-
delete this.agentStatusTimers[id]
|
|
260
|
-
delete this.activeAgentTasks?.[id]
|
|
261
|
-
this.syncGlobalAgentTasks()
|
|
262
|
-
this.renderTypingIndicator()
|
|
263
|
-
}, AGENT_STATUS_TIMEOUT)
|
|
264
|
-
} else {
|
|
265
|
-
delete this.typingUsers[id]
|
|
266
|
-
delete this.activeAgentTasks?.[id]
|
|
267
|
-
if (this.agentStatusTimers?.[id]) {
|
|
268
|
-
clearTimeout(this.agentStatusTimers[id])
|
|
269
|
-
delete this.agentStatusTimers[id]
|
|
415
|
+
this.agentStates[id] = status
|
|
416
|
+
if (!this.activeAgentTasks[id]) this.activeAgentTasks[id] = []
|
|
417
|
+
if (task_id && !this.activeAgentTasks[id].includes(task_id)) {
|
|
418
|
+
this.activeAgentTasks[id].push(task_id)
|
|
270
419
|
}
|
|
420
|
+
// Streaming heartbeat: transition to thinking if no update within timeout
|
|
421
|
+
this.clearStreamingHeartbeat(id)
|
|
422
|
+
if (status === 'streaming') {
|
|
423
|
+
this.streamingHeartbeatTimers[id] = setTimeout(() => {
|
|
424
|
+
this.agentStates[id] = 'thinking'
|
|
425
|
+
delete this.streamingHeartbeatTimers[id]
|
|
426
|
+
this.renderTypingIndicator()
|
|
427
|
+
}, STREAMING_HEARTBEAT_TIMEOUT)
|
|
428
|
+
}
|
|
429
|
+
this.startAgentTaskPoll()
|
|
430
|
+
} else {
|
|
431
|
+
this.dropAgentTask(id, task_id)
|
|
432
|
+
this.maybeStopAgentTaskPoll()
|
|
271
433
|
}
|
|
272
434
|
this.syncGlobalAgentTasks()
|
|
273
435
|
this.renderTypingIndicator({ newItem: isNewAgent })
|
|
@@ -384,7 +546,7 @@ export default class extends Controller {
|
|
|
384
546
|
this.typingIndicatorTarget.style.opacity = '1'
|
|
385
547
|
|
|
386
548
|
// Add stop button first (before avatars/names) for active agent tasks
|
|
387
|
-
const hasActiveTask = ids.some((id) => this.activeAgentTasks
|
|
549
|
+
const hasActiveTask = ids.some((id) => this.activeAgentTasks[id]?.length > 0)
|
|
388
550
|
if (hasActiveTask) {
|
|
389
551
|
const stopBtn = document.createElement('button')
|
|
390
552
|
stopBtn.type = 'button'
|
|
@@ -394,8 +556,11 @@ export default class extends Controller {
|
|
|
394
556
|
stopBtn.title = stopLabel
|
|
395
557
|
stopBtn.addEventListener('click', () => {
|
|
396
558
|
ids.forEach((id) => {
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
559
|
+
const tasks = this.activeAgentTasks[id]
|
|
560
|
+
if (tasks?.length > 0) {
|
|
561
|
+
// Cancel the last (most recent) task
|
|
562
|
+
this.cancelAgentTask(tasks[tasks.length - 1], id)
|
|
563
|
+
}
|
|
399
564
|
})
|
|
400
565
|
})
|
|
401
566
|
this.typingIndicatorTarget.appendChild(stopBtn)
|
|
@@ -425,9 +590,13 @@ export default class extends Controller {
|
|
|
425
590
|
this.typingIndicatorTarget.appendChild(wrapper)
|
|
426
591
|
})
|
|
427
592
|
}
|
|
428
|
-
const names = ids.map((id) => this.typingUsers[id])
|
|
429
593
|
const text = document.createElement('span')
|
|
430
|
-
|
|
594
|
+
const parts = ids.map((id) => {
|
|
595
|
+
const name = this.typingUsers[id]
|
|
596
|
+
const isAgentThinking = this.activeAgentTasks[id]?.length > 0 && this.agentStates[id] !== 'streaming'
|
|
597
|
+
return isAgentThinking ? `${name} \u23F3` : `${name} ...`
|
|
598
|
+
})
|
|
599
|
+
text.textContent = parts.join(' ')
|
|
431
600
|
this.typingIndicatorTarget.appendChild(text)
|
|
432
601
|
|
|
433
602
|
if (stickToEnd) this.scrollRowToEnd()
|
|
@@ -463,11 +632,13 @@ export default class extends Controller {
|
|
|
463
632
|
}
|
|
464
633
|
|
|
465
634
|
cancelAllAgentTasks() {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
635
|
+
const agentIds = Object.keys(this.activeAgentTasks)
|
|
636
|
+
if (agentIds.length === 0) return false
|
|
637
|
+
agentIds.forEach((agentId) => {
|
|
638
|
+
const tasks = this.activeAgentTasks[agentId]
|
|
639
|
+
if (tasks?.length > 0) {
|
|
640
|
+
this.cancelAgentTask(tasks[tasks.length - 1], agentId)
|
|
641
|
+
}
|
|
471
642
|
})
|
|
472
643
|
return true
|
|
473
644
|
}
|
|
@@ -479,12 +650,8 @@ export default class extends Controller {
|
|
|
479
650
|
})
|
|
480
651
|
.then((response) => {
|
|
481
652
|
if (response.ok) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (this.agentStatusTimers?.[agentId]) {
|
|
485
|
-
clearTimeout(this.agentStatusTimers[agentId])
|
|
486
|
-
delete this.agentStatusTimers[agentId]
|
|
487
|
-
}
|
|
653
|
+
this.dropAgentTask(agentId, taskId)
|
|
654
|
+
this.maybeStopAgentTaskPoll()
|
|
488
655
|
this.syncGlobalAgentTasks()
|
|
489
656
|
this.renderTypingIndicator()
|
|
490
657
|
}
|
|
@@ -548,7 +715,9 @@ export default class extends Controller {
|
|
|
548
715
|
csrfFetch(`/channels/${id}`, {
|
|
549
716
|
method: 'DELETE',
|
|
550
717
|
headers: { Accept: 'application/json' },
|
|
551
|
-
})
|
|
718
|
+
})
|
|
719
|
+
.then((response) => response.status === 204 && !response.redirected && btn.closest('.channel-chip')?.remove())
|
|
720
|
+
.catch((err) => console.warn('[presence] detach channel failed:', err))
|
|
552
721
|
}
|
|
553
722
|
|
|
554
723
|
clearTypingTimers() {
|
|
@@ -556,6 +725,99 @@ export default class extends Controller {
|
|
|
556
725
|
this.typingTimers = {}
|
|
557
726
|
}
|
|
558
727
|
|
|
728
|
+
// Every indicator on screen belongs to the topic being left, and the replay that
|
|
729
|
+
// follows is scoped to the new one — so the whole agent state goes, heartbeats and
|
|
730
|
+
// poll included. A surviving poll would keep resurrecting the old topic's turns.
|
|
731
|
+
clearTopicIndicators() {
|
|
732
|
+
this.resetAgentActivity()
|
|
733
|
+
this.renderTypingIndicator()
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
get typingTopicId() {
|
|
737
|
+
return this.selectedTopicId || this.mainTopicId
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
isSelectedTopic(topicId) {
|
|
741
|
+
return Boolean(
|
|
742
|
+
this.selectedTopicId &&
|
|
743
|
+
topicId &&
|
|
744
|
+
String(topicId) === String(this.selectedTopicId)
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ── Streaming heartbeat ─────────────────────────────────
|
|
749
|
+
|
|
750
|
+
clearStreamingHeartbeat(agentId) {
|
|
751
|
+
if (this.streamingHeartbeatTimers[agentId]) {
|
|
752
|
+
clearTimeout(this.streamingHeartbeatTimers[agentId])
|
|
753
|
+
delete this.streamingHeartbeatTimers[agentId]
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
clearAllStreamingHeartbeats() {
|
|
758
|
+
Object.values(this.streamingHeartbeatTimers).forEach((timer) => clearTimeout(timer))
|
|
759
|
+
this.streamingHeartbeatTimers = {}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ── Agent task status polling ──────────────────────────
|
|
763
|
+
|
|
764
|
+
startAgentTaskPoll() {
|
|
765
|
+
if (this.agentTaskPollHandle) return
|
|
766
|
+
this.agentTaskPollHandle = setInterval(() => this.pollAgentTaskStatuses(), AGENT_TASK_POLL_INTERVAL)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
stopAgentTaskPoll() {
|
|
770
|
+
if (this.agentTaskPollHandle) {
|
|
771
|
+
clearInterval(this.agentTaskPollHandle)
|
|
772
|
+
this.agentTaskPollHandle = null
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
maybeStopAgentTaskPoll() {
|
|
777
|
+
if (Object.keys(this.activeAgentTasks).length === 0) this.stopAgentTaskPoll()
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
pollAgentTaskStatuses() {
|
|
781
|
+
const allTaskIds = Object.values(this.activeAgentTasks).flat()
|
|
782
|
+
if (allTaskIds.length === 0) {
|
|
783
|
+
this.stopAgentTaskPoll()
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
const requestedTaskIds = new Set(allTaskIds)
|
|
787
|
+
|
|
788
|
+
csrfFetch(`/tasks/active_statuses?task_ids=${allTaskIds.join(',')}`, {
|
|
789
|
+
headers: { Accept: 'application/json' },
|
|
790
|
+
})
|
|
791
|
+
.then((response) => {
|
|
792
|
+
if (!response.ok) return null
|
|
793
|
+
return response.json()
|
|
794
|
+
})
|
|
795
|
+
.then((data) => {
|
|
796
|
+
if (!data) return
|
|
797
|
+
// Task#active? decides what still deserves an indicator and a Stop
|
|
798
|
+
// button, so the client never has to know the status vocabulary.
|
|
799
|
+
const activeTaskIds = new Set(data.tasks.filter((t) => t.active).map((t) => t.id))
|
|
800
|
+
|
|
801
|
+
// This response only speaks for the ids it was asked about. A task that
|
|
802
|
+
// an agent_status message appended while the request was in flight was
|
|
803
|
+
// never requested, so its absence here means nothing — keep it until a
|
|
804
|
+
// poll that actually asked about it says otherwise.
|
|
805
|
+
let changed = false
|
|
806
|
+
Object.keys(this.activeAgentTasks).forEach((agentId) => {
|
|
807
|
+
const before = this.activeAgentTasks[agentId].length
|
|
808
|
+
this.activeAgentTasks[agentId] = this.activeAgentTasks[agentId].filter(
|
|
809
|
+
(taskId) => activeTaskIds.has(taskId) || !requestedTaskIds.has(taskId),
|
|
810
|
+
)
|
|
811
|
+
if (this.activeAgentTasks[agentId].length !== before) changed = true
|
|
812
|
+
if (this.activeAgentTasks[agentId].length === 0) this.dropAgent(agentId)
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
if (changed) this.renderTypingIndicator()
|
|
816
|
+
this.maybeStopAgentTaskPoll()
|
|
817
|
+
})
|
|
818
|
+
.catch((err) => console.warn('[presence] poll agent task statuses failed:', err))
|
|
819
|
+
}
|
|
820
|
+
|
|
559
821
|
handleInput() {
|
|
560
822
|
this.clearManualTypingMessage()
|
|
561
823
|
this.typing()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Tracks where the "previous message" button left off.
|
|
2
|
+
//
|
|
3
|
+
// The button scrolls with `behavior: 'smooth'`, so a rapid second click arrives
|
|
4
|
+
// while the list is still mid-animation. Deriving the current position purely
|
|
5
|
+
// from geometry at that moment picks the item we just left (it has not reached
|
|
6
|
+
// the top yet), which makes the list bounce up and down instead of walking
|
|
7
|
+
// backwards. Remembering the last target as an anchor keeps each click stepping
|
|
8
|
+
// one message further back.
|
|
9
|
+
//
|
|
10
|
+
// The anchor is dropped once the user moves the list themselves, so geometry
|
|
11
|
+
// takes over from wherever they landed. Invalidation is driven by input events
|
|
12
|
+
// rather than scroll events: a scroll event cannot be attributed to the user
|
|
13
|
+
// without guessing how long our own animation runs, and guessing short brings
|
|
14
|
+
// the bounce back. Dropping the anchor is always the safe direction — a settled
|
|
15
|
+
// list resolves correctly from geometry alone.
|
|
16
|
+
//
|
|
17
|
+
// Kept free of DOM access so it can be unit tested: jsdom has no layout engine,
|
|
18
|
+
// so the controller measures the items and passes `{ id, top }` pairs in.
|
|
19
|
+
|
|
20
|
+
// Sub-pixel scroll offsets (HiDPI, browser rounding) mean an item parked at the
|
|
21
|
+
// top is rarely at exactly `viewportTop`.
|
|
22
|
+
const TOP_TOLERANCE = 4
|
|
23
|
+
|
|
24
|
+
export default class PrevMessageNavigator {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.anchorId = null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// items: `{ id, top }` in DOM order (oldest first), top relative to viewport.
|
|
30
|
+
// Returns the index to scroll to, or -1 when there is nothing older in the
|
|
31
|
+
// list (the caller then loads older comments).
|
|
32
|
+
resolveTargetIndex(items, viewportTop) {
|
|
33
|
+
if (items.length === 0) return -1
|
|
34
|
+
|
|
35
|
+
if (this.anchorId !== null) {
|
|
36
|
+
const anchorIdx = items.findIndex((item) => item.id === this.anchorId)
|
|
37
|
+
if (anchorIdx !== -1 && this.anchorIsOnItsWay(items[anchorIdx].top, viewportTop)) {
|
|
38
|
+
return anchorIdx - 1
|
|
39
|
+
}
|
|
40
|
+
this.anchorId = null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let currentIdx = items.findIndex((item) => item.top >= viewportTop - TOP_TOLERANCE)
|
|
44
|
+
if (currentIdx === -1) currentIdx = items.length - 1
|
|
45
|
+
|
|
46
|
+
const isAtTop = Math.abs(items[currentIdx].top - viewportTop) < TOP_TOLERANCE
|
|
47
|
+
return isAtTop ? currentIdx - 1 : currentIdx
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The anchor travels from wherever it sat when we committed to `viewportTop`,
|
|
51
|
+
// and nothing else moves it. Outside that corridor the list was scrolled by
|
|
52
|
+
// something other than us - a new message arriving, a permalink jump - none of
|
|
53
|
+
// which fire a user input event, so the anchor would otherwise survive and wedge
|
|
54
|
+
// the button (anchored on the oldest item, every click resolves to -1 forever).
|
|
55
|
+
//
|
|
56
|
+
// The corridor spans both endpoints because the anchor can approach the top from
|
|
57
|
+
// either side: usually from above (the next message back), but from below when
|
|
58
|
+
// the click started between messages and we pulled the partially visible one up.
|
|
59
|
+
//
|
|
60
|
+
// Comparing the anchor against its own start position rather than the list's
|
|
61
|
+
// scroll offset keeps this correct when older comments are prepended: that
|
|
62
|
+
// shifts `scrollTop` but leaves the anchor where it is on screen.
|
|
63
|
+
anchorIsOnItsWay(anchorTop, viewportTop) {
|
|
64
|
+
const low = Math.min(this.anchorStartTop, viewportTop) - TOP_TOLERANCE
|
|
65
|
+
const high = Math.max(this.anchorStartTop, viewportTop) + TOP_TOLERANCE
|
|
66
|
+
return anchorTop >= low && anchorTop <= high
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Called once we start scrolling towards `id`, which currently sits at `top`.
|
|
70
|
+
commit(id, top) {
|
|
71
|
+
this.anchorId = id
|
|
72
|
+
this.anchorStartTop = top
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Called when the user interacts with the list (wheel, touch, key, pointer).
|
|
76
|
+
notifyUserInput() {
|
|
77
|
+
this.anchorId = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Called when our own code repositions the list without going through the
|
|
81
|
+
// button - jumping to the newest message, a permalink highlight. These fire no
|
|
82
|
+
// user input event, and a small jump can leave the anchor inside the corridor
|
|
83
|
+
// above, where geometry cannot tell it apart from our own in-flight scroll. The
|
|
84
|
+
// corridor still catches larger jumps, but the caller drops the anchor at these
|
|
85
|
+
// known sites so a still-visible message is never skipped.
|
|
86
|
+
notifyProgrammaticScroll() {
|
|
87
|
+
this.anchorId = null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Called when the list contents are replaced wholesale.
|
|
91
|
+
reset() {
|
|
92
|
+
this.anchorId = null
|
|
93
|
+
}
|
|
94
|
+
}
|