collavre 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
- data/app/assets/stylesheets/collavre/creatives.css +346 -37
- data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
- data/app/assets/stylesheets/collavre/gnb.css +64 -3
- data/app/assets/stylesheets/collavre/landing.css +108 -3
- data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +14 -0
- data/app/assets/stylesheets/collavre/print.css +0 -1
- data/app/assets/stylesheets/collavre/search_popup.css +1 -1
- data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
- data/app/assets/stylesheets/collavre/workspace.css +307 -0
- data/app/channels/collavre/agent_channel.rb +14 -21
- data/app/channels/collavre/comments_presence_channel.rb +181 -21
- data/app/channels/collavre/inbox_badge_channel.rb +7 -1
- data/app/components/collavre/inbox/badge_component.rb +6 -13
- data/app/components/collavre/progress_filter_component.html.erb +2 -2
- data/app/components/collavre/progress_filter_component.rb +20 -0
- data/app/controllers/collavre/admin/settings_controller.rb +4 -1
- data/app/controllers/collavre/agent_connections_controller.rb +165 -0
- data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
- data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
- data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
- data/app/controllers/collavre/application_controller.rb +13 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
- data/app/controllers/collavre/comments_controller.rb +45 -131
- data/app/controllers/collavre/concerns/shareable.rb +9 -3
- data/app/controllers/collavre/creatives_controller.rb +144 -11
- data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
- data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
- data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
- data/app/controllers/collavre/features_controller.rb +57 -0
- data/app/controllers/collavre/invites_controller.rb +12 -1
- data/app/controllers/collavre/llm_models_controller.rb +8 -0
- data/app/controllers/collavre/notices_controller.rb +20 -0
- data/app/controllers/collavre/tasks_controller.rb +40 -6
- data/app/controllers/collavre/topics_controller.rb +130 -53
- data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
- data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
- data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
- data/app/errors/collavre/turn_deadline_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +38 -0
- data/app/helpers/collavre/creatives_helper.rb +56 -22
- data/app/helpers/collavre/navigation_helper.rb +12 -0
- data/app/javascript/collavre.js +1 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
- data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
- data/app/javascript/components/creative_tree_row.js +62 -22
- data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
- data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
- data/app/javascript/controllers/__tests__/index.test.js +42 -0
- data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
- data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
- data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
- data/app/javascript/controllers/agent_connection_controller.js +357 -0
- data/app/javascript/controllers/agent_vendor_controller.js +23 -0
- data/app/javascript/controllers/comment_controller.js +3 -1
- data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
- data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
- data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
- data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
- data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
- data/app/javascript/controllers/comments/form_controller.js +795 -14
- data/app/javascript/controllers/comments/list_controller.js +338 -52
- data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
- data/app/javascript/controllers/comments/popup_controller.js +425 -83
- data/app/javascript/controllers/comments/presence_controller.js +317 -55
- data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
- data/app/javascript/controllers/comments/topics_controller.js +1772 -100
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
- data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
- data/app/javascript/controllers/creatives/tree_controller.js +140 -31
- data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
- data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
- data/app/javascript/controllers/gateway_check_controller.js +36 -0
- data/app/javascript/controllers/index.js +25 -1
- data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
- data/app/javascript/controllers/link_creative_controller.js +89 -18
- data/app/javascript/controllers/progress_filter_controller.js +12 -22
- data/app/javascript/controllers/search_popup_controller.js +64 -46
- data/app/javascript/controllers/share_user_search_controller.js +2 -2
- data/app/javascript/controllers/topic_list_controller.js +44 -7
- data/app/javascript/controllers/workspace_tree_controller.js +662 -0
- data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
- data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
- data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
- data/app/javascript/creatives/tree_renderer.js +65 -22
- data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
- data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
- data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
- data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
- data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
- data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
- data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
- data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
- data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
- data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
- data/app/javascript/lib/api/creatives.js +32 -13
- data/app/javascript/lib/api/csrf_fetch.js +6 -4
- data/app/javascript/lib/api/topics.js +63 -5
- data/app/javascript/lib/chat_drafts.js +786 -0
- data/app/javascript/lib/common_popup.js +164 -25
- data/app/javascript/lib/creative_link_prefetch.js +27 -0
- data/app/javascript/lib/last_visited_creative.js +97 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
- data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
- data/app/javascript/lib/lexical/creative_link_node.js +101 -0
- data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
- data/app/javascript/lib/lexical/link_toolbar.js +23 -0
- data/app/javascript/lib/turbo_stream_actions.js +23 -16
- data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
- data/app/javascript/lib/utils/filter_navigation.js +100 -0
- data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
- data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
- data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
- data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
- data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
- data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
- data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
- data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
- data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
- data/app/javascript/modules/command_args_form.js +0 -12
- data/app/javascript/modules/command_menu.js +80 -38
- data/app/javascript/modules/creative_link_picker.js +56 -0
- data/app/javascript/modules/creative_row_editor.js +446 -243
- data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
- data/app/javascript/modules/creative_tree_dom.js +4 -0
- data/app/javascript/modules/creative_tree_empty_state.js +80 -0
- data/app/javascript/modules/dom_listener_registry.js +23 -0
- data/app/javascript/modules/mention_menu.js +2 -3
- data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
- data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
- data/app/javascript/utils/caret_position.js +10 -0
- data/app/javascript/utils/chevron_icons.js +7 -0
- data/app/jobs/collavre/ai_agent_job.rb +262 -72
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
- data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
- data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
- data/app/jobs/collavre/comment_notification_job.rb +14 -0
- data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
- data/app/jobs/collavre/compress_job.rb +33 -22
- data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
- data/app/jobs/collavre/cron_action_job.rb +36 -18
- data/app/jobs/collavre/drop_trigger_job.rb +23 -9
- data/app/jobs/collavre/merge_comments_job.rb +33 -16
- data/app/jobs/collavre/push_notification_job.rb +62 -18
- data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
- data/app/jobs/collavre/stuck_detector_job.rb +1 -1
- data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
- data/app/models/collavre/agent_gateway.rb +200 -0
- data/app/models/collavre/agent_subscription.rb +4 -4
- data/app/models/collavre/agent_workspace.rb +210 -0
- data/app/models/collavre/comment/broadcastable.rb +22 -52
- data/app/models/collavre/comment/notifiable.rb +154 -77
- data/app/models/collavre/comment/topic_membership.rb +41 -0
- data/app/models/collavre/comment.rb +336 -31
- data/app/models/collavre/comment_notification_delivery.rb +61 -0
- data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
- data/app/models/collavre/comment_presence_store.rb +272 -12
- data/app/models/collavre/comment_read_pointer.rb +9 -15
- data/app/models/collavre/creative/describable.rb +8 -2
- data/app/models/collavre/creative/permissible.rb +11 -0
- data/app/models/collavre/creative.rb +29 -4
- data/app/models/collavre/creative_share.rb +109 -4
- data/app/models/collavre/current.rb +1 -0
- data/app/models/collavre/llm_model.rb +62 -0
- data/app/models/collavre/system_setting.rb +12 -1
- data/app/models/collavre/task.rb +121 -2
- data/app/models/collavre/topic.rb +51 -0
- data/app/models/collavre/user.rb +101 -41
- data/app/models/collavre/user_creative_preference.rb +3 -1
- data/app/services/collavre/agent_provisioning/archive.rb +108 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
- data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
- data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
- data/app/services/collavre/ai_agent/message_builder.rb +103 -16
- data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
- data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
- data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
- data/app/services/collavre/ai_agent_service.rb +108 -20
- data/app/services/collavre/ai_client.rb +211 -21
- data/app/services/collavre/cli_proxy/client.rb +147 -0
- data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
- data/app/services/collavre/cli_proxy/identity.rb +47 -0
- data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
- data/app/services/collavre/command_menu_service.rb +0 -10
- data/app/services/collavre/comment_move_service.rb +96 -7
- data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
- data/app/services/collavre/comments/action_executor.rb +48 -20
- data/app/services/collavre/comments/command_processor.rb +0 -1
- data/app/services/collavre/comments/list_response.rb +133 -0
- data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
- data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
- data/app/services/collavre/comments/read_receipt_index.rb +152 -0
- data/app/services/collavre/comments/topic_command.rb +72 -1
- data/app/services/collavre/comments/topic_mutation.rb +20 -0
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
- data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
- data/app/services/collavre/creatives/reorderer.rb +15 -3
- data/app/services/collavre/creatives/tree_builder.rb +1 -1
- data/app/services/collavre/creatives/tree_formatter.rb +5 -6
- data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
- data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
- data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
- data/app/services/collavre/home_page_path.rb +55 -0
- data/app/services/collavre/html_text.rb +64 -0
- data/app/services/collavre/http_client.rb +11 -2
- data/app/services/collavre/inbox_reply_service.rb +1 -1
- data/app/services/collavre/markdown_converter.rb +13 -1
- data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
- data/app/services/collavre/orchestration/arbiter.rb +48 -5
- data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
- data/app/services/collavre/orchestration/delivery_record.rb +792 -0
- data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
- data/app/services/collavre/orchestration/matcher.rb +132 -17
- data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
- data/app/services/collavre/orchestration/scheduler.rb +10 -4
- data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
- data/app/services/collavre/orchestration/selection.rb +43 -0
- data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
- data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
- data/app/services/collavre/orchestration/topic_slot.rb +73 -0
- data/app/services/collavre/system_events/context_builder.rb +60 -11
- data/app/services/collavre/system_events/dispatcher.rb +58 -12
- data/app/services/collavre/system_events/envelope.rb +73 -0
- data/app/services/collavre/system_events/vocabulary.rb +45 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
- data/app/services/collavre/tools/cron_create_service.rb +32 -18
- data/app/services/collavre/tools/id_list.rb +38 -0
- data/app/services/collavre/tools/topic_authorizer.rb +61 -7
- data/app/services/collavre/tools/topic_branch_service.rb +85 -0
- data/app/services/collavre/tools/topic_create_service.rb +117 -0
- data/app/services/collavre/tools/topic_list_service.rb +101 -0
- data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
- data/app/services/collavre/tools/topic_messages_service.rb +346 -0
- data/app/services/collavre/tools/topic_move_service.rb +108 -0
- data/app/services/collavre/tools/topic_selection.rb +59 -0
- data/app/services/collavre/tools/topic_update_service.rb +165 -0
- data/app/services/collavre/topic_branch_service.rb +39 -10
- data/app/services/collavre/topics/agent_resolver.rb +87 -0
- data/app/services/collavre/topics/char_budget.rb +70 -0
- data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
- data/app/services/collavre/topics/message_page.rb +380 -0
- data/app/services/collavre/topics/message_page_markdown.rb +102 -0
- data/app/services/collavre/topics/message_scope.rb +61 -0
- data/app/services/collavre/topics/message_stats.rb +92 -0
- data/app/services/collavre/topics/move_blocker.rb +47 -0
- data/app/services/collavre/topics/next_name.rb +35 -0
- data/app/services/collavre/topics/pin_rejection.rb +25 -0
- data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
- data/app/services/collavre/topics/reserved_name.rb +18 -0
- data/app/services/collavre/topics/serializer.rb +92 -0
- data/app/services/collavre/topics/topic_destroy.rb +30 -0
- data/app/services/collavre/topics/topic_move.rb +80 -0
- data/app/services/collavre/topics/topic_move_effects.rb +35 -0
- data/app/services/collavre/topics/topic_mutation.rb +27 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
- data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
- data/app/views/collavre/agent_connections/show.html.erb +77 -0
- data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
- data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
- data/app/views/collavre/agent_gateways/index.html.erb +54 -0
- data/app/views/collavre/agent_gateways/new.html.erb +2 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
- data/app/views/collavre/comments/_empty_state.html.erb +33 -0
- data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
- data/app/views/collavre/comments/_feature_card.html.erb +35 -0
- data/app/views/collavre/comments/_list.html.erb +3 -2
- data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
- data/app/views/collavre/creatives/_add_button.html.erb +17 -19
- data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
- data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
- data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
- data/app/views/collavre/creatives/index.html.erb +79 -18
- data/app/views/collavre/desktop_setup/show.html.erb +114 -0
- data/app/views/collavre/features/index.html.erb +42 -0
- data/app/views/collavre/features/show.html.erb +52 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
- data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
- data/app/views/collavre/invites/show.html.erb +14 -1
- data/app/views/collavre/landing/show.html.erb +5 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
- data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
- data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
- data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
- data/app/views/collavre/user_themes/index.html.erb +1 -1
- data/app/views/collavre/users/_llm_fields.html.erb +65 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
- data/app/views/collavre/users/edit_ai.html.erb +33 -31
- data/app/views/collavre/users/new.html.erb +4 -2
- data/app/views/collavre/users/new_ai.html.erb +14 -32
- data/app/views/collavre/users/show.html.erb +13 -1
- data/app/views/layouts/collavre/landing.html.erb +9 -4
- data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
- data/config/locales/admin.en.yml +3 -1
- data/config/locales/admin.ko.yml +3 -1
- data/config/locales/agent_gateway.en.yml +217 -0
- data/config/locales/agent_gateway.ko.yml +217 -0
- data/config/locales/ai_agent.en.yml +0 -1
- data/config/locales/ai_agent.ko.yml +0 -1
- data/config/locales/comments.en.yml +72 -30
- data/config/locales/comments.ko.yml +72 -30
- data/config/locales/creatives.en.yml +32 -1
- data/config/locales/creatives.ko.yml +32 -1
- data/config/locales/features.en.yml +140 -0
- data/config/locales/features.ko.yml +140 -0
- data/config/locales/invites.en.yml +3 -0
- data/config/locales/invites.ko.yml +3 -0
- data/config/locales/landing.en.yml +2 -1
- data/config/locales/landing.ko.yml +2 -1
- data/config/locales/tools.en.yml +20 -0
- data/config/locales/tools.ko.yml +20 -0
- data/config/locales/users.en.yml +15 -14
- data/config/locales/users.ko.yml +14 -14
- data/config/routes.rb +45 -19
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
- data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
- data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
- data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
- data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
- data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
- data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
- data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
- data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
- data/db/migrate/20260809000001_create_llm_models.rb +64 -0
- data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
- data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
- data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
- data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
- data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
- data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
- data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
- data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
- data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
- data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
- data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
- data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
- data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
- data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
- data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
- data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
- data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
- data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
- data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
- data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
- data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
- data/db/seeds.rb +1 -51
- data/lib/collavre/engine.rb +108 -1
- data/lib/collavre/feature_card_registry.rb +168 -0
- data/lib/collavre/hashed_access_token_lookup.rb +22 -0
- data/lib/collavre/sensitive_request_silencer.rb +23 -0
- data/lib/collavre/user_extensions.rb +0 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +1 -0
- data/skills/collavre/SKILL.md +165 -0
- data/skills/collavre/package.json +8 -0
- data/skills/collavre/references/tool-reference.md +347 -0
- data/skills/collavre/scripts/collavre +634 -0
- data/skills/collavre/scripts/html_entities.js +24 -0
- metadata +228 -30
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
- data/app/controllers/collavre/inbox_items_controller.rb +0 -70
- data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
- data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
- data/app/javascript/lib/typo_correction.js +0 -146
- data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
- data/app/javascript/modules/creative_guide.js +0 -67
- data/app/javascript/modules/typo_corrector.js +0 -534
- data/app/jobs/collavre/inbox_summary_job.rb +0 -24
- data/app/mailers/collavre/inbox_mailer.rb +0 -19
- data/app/models/collavre/inbox_item.rb +0 -89
- data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
- data/app/services/collavre/comments/work_command.rb +0 -161
- data/app/services/collavre/comments/workflow_executor.rb +0 -276
- data/app/services/collavre/gemini_parent_recommender.rb +0 -77
- data/app/services/collavre/mobile/event_summarizer.rb +0 -40
- data/app/services/collavre/typo_corrector.rb +0 -188
- data/app/views/collavre/inbox_items/_item.html.erb +0 -18
- data/app/views/collavre/inbox_items/_items.html.erb +0 -3
- data/app/views/collavre/inbox_items/_list.html.erb +0 -7
- data/app/views/collavre/inbox_items/index.html.erb +0 -1
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
- data/app/views/collavre/users/typo_correction.html.erb +0 -50
- data/config/locales/mobile.en.yml +0 -16
- data/config/locales/mobile.ko.yml +0 -16
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Collavre
|
|
4
|
-
module Api
|
|
5
|
-
module V1
|
|
6
|
-
module Mobile
|
|
7
|
-
# The heart of the loop. GET surfaces pending approvals (+ recent agent
|
|
8
|
-
# replies) with a stable spoken ref number and a decision-ready summary.
|
|
9
|
-
# POST :id/respond branches on the referenced event KIND:
|
|
10
|
-
# (A) approval/permission comment → the spoken response is a BUTTON
|
|
11
|
-
# press (allow|deny) → decide_claude_channel_permission! + relay.
|
|
12
|
-
# (B) ordinary agent message → the spoken response is FREE TEXT passed
|
|
13
|
-
# verbatim back to the agent as a reply comment (no server parsing).
|
|
14
|
-
class AgentEventsController < BaseController
|
|
15
|
-
# Spoken decision verbs. The app no longer addresses approvals by
|
|
16
|
-
# ordinal, so a reply to an approval event is just an allow/deny verb;
|
|
17
|
-
# anything else asks for clarification rather than firing a decision.
|
|
18
|
-
APPROVE = /(승인|허용|적용|approve|allow|confirm|네|좋아|오케이|\bok\b|\byes\b)/i
|
|
19
|
-
DENY = /(거절|거부|반려|deny|reject|취소|아니|\bno\b)/i
|
|
20
|
-
|
|
21
|
-
def index
|
|
22
|
-
approvals = pending_approvals
|
|
23
|
-
notices = system_inbox_messages
|
|
24
|
-
|
|
25
|
-
# Every undecided approval is emitted on every poll — the server keeps
|
|
26
|
-
# no per-client cursor. Re-speaking is prevented on the CLIENT (an
|
|
27
|
-
# in-memory "already spoken" set), the same at-least-once shape as
|
|
28
|
-
# notices: a pending approval keeps surfacing until it is decided
|
|
29
|
-
# (which clears it from pending_approvals) or the app restarts (when a
|
|
30
|
-
# still-pending approval SHOULD be re-surfaced). The old `since` cursor
|
|
31
|
-
# filtered here, but a batch high-water-mark could burn past an approval
|
|
32
|
-
# whose created_at trailed a newer notice, losing it forever.
|
|
33
|
-
events = approvals.map do |c|
|
|
34
|
-
{
|
|
35
|
-
id: c.id, type: "approval_requested",
|
|
36
|
-
title: title_for_topic(c.topic_id),
|
|
37
|
-
summary: summarizer.approval_summary(comment: c, label: label_for_topic(c.topic_id)),
|
|
38
|
-
speak: true, requires_response: true, topic_id: c.topic_id,
|
|
39
|
-
created_at: c.created_at.iso8601(6)
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
events += notices.map do |c|
|
|
44
|
-
# The notice stands in for the origin comment it quotes; the app
|
|
45
|
-
# lists/reads it against the ORIGIN thread and replies route there.
|
|
46
|
-
origin_topic_id = c.quoted_comment&.topic_id || c.topic_id
|
|
47
|
-
{
|
|
48
|
-
id: c.id, type: "agent_reply",
|
|
49
|
-
title: title_for_topic(origin_topic_id),
|
|
50
|
-
summary: speakable(c.content),
|
|
51
|
-
speak: true, requires_response: c.quoted_comment_id.present?,
|
|
52
|
-
topic_id: origin_topic_id, created_at: c.created_at.iso8601(6)
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
render json: events.sort_by { |e| e[:created_at] }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def respond
|
|
60
|
-
comment = Collavre::Comment.find_by(id: params[:id])
|
|
61
|
-
unless comment && authorized_comment?(comment)
|
|
62
|
-
return render json: { error: "Event not found" }, status: :not_found
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Acting on a notice means the user has heard it — clear it from the
|
|
66
|
-
# unread inbox set so it isn't read again on the next poll.
|
|
67
|
-
mark_inbox_read(comment)
|
|
68
|
-
|
|
69
|
-
if comment.claude_channel_permission?
|
|
70
|
-
decide_on(comment)
|
|
71
|
-
else
|
|
72
|
-
relay_free_text(comment)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# The app calls this when it finishes reading a message ALOUD (whether or
|
|
77
|
-
# not the user then replies). Reading = hearing it, so the inbox read
|
|
78
|
-
# pointer advances and the notice stops surfacing — the SAME read-state
|
|
79
|
-
# the inbox badge uses. A crash before this call leaves the notice unread,
|
|
80
|
-
# so the next poll re-reads it (at-least-once, not the lossy old cursor).
|
|
81
|
-
def read
|
|
82
|
-
comment = Collavre::Comment.find_by(id: params[:id])
|
|
83
|
-
unless comment && authorized_comment?(comment)
|
|
84
|
-
return render json: { error: "Event not found" }, status: :not_found
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
mark_inbox_read(comment)
|
|
88
|
-
head :ok
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
# The user's Inbox → System topic is the per-user alarm stream: mentions,
|
|
94
|
-
# agent replies, share notices, … land here as system-authored
|
|
95
|
-
# (user_id: nil) comments that QUOTE the origin comment. This is the
|
|
96
|
-
# canonical, agent-ownership-independent feed the voice loop reads aloud.
|
|
97
|
-
#
|
|
98
|
-
# Emission is gated by the inbox's OWN read-state — the CommentReadPointer
|
|
99
|
-
# (comments with id > last_read_comment_id are unread) that also drives the
|
|
100
|
-
# inbox badge — NOT by the client's `since` cursor. A `since` cursor is a
|
|
101
|
-
# fetch-time high-water-mark: any notice fetched-but-never-spoken (crash,
|
|
102
|
-
# background, busy queue, restart) gets burned past and is lost forever,
|
|
103
|
-
# even though it stays unread in the inbox. Driving off the read pointer
|
|
104
|
-
# makes "unread in the inbox" and "read aloud by the app" the same set; the
|
|
105
|
-
# app marks a notice read (POST :id/read) only after actually speaking it.
|
|
106
|
-
def system_inbox_messages
|
|
107
|
-
inbox = current_user_inbox or return []
|
|
108
|
-
last_read = inbox_read_pointer&.last_read_comment_id || 0
|
|
109
|
-
|
|
110
|
-
scope = Collavre::Comment.where(creative_id: inbox.id, topic_id: inbox.system_topic.id)
|
|
111
|
-
.where("comments.id > ?", last_read)
|
|
112
|
-
.order(:id)
|
|
113
|
-
.limit(50)
|
|
114
|
-
# Approval prompts are surfaced (actionably) by pending_approvals; their
|
|
115
|
-
# System-topic FYI duplicate must not be read a second time.
|
|
116
|
-
scope.reject { |c| c.quoted_comment&.claude_channel_permission? }
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def inbox_read_pointer
|
|
120
|
-
inbox = current_user_inbox or return nil
|
|
121
|
-
|
|
122
|
-
Collavre::CommentReadPointer.find_by(user: current_user, creative: inbox)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Advance the inbox read pointer to (at least) this notice — forward-only,
|
|
126
|
-
# so a later message is never un-read. Only inbox-creative comments move the
|
|
127
|
-
# inbox pointer; calling this for an approval (which lives in the origin
|
|
128
|
-
# creative and is owned by pending_approvals) is a harmless no-op.
|
|
129
|
-
def mark_inbox_read(comment)
|
|
130
|
-
inbox = current_user_inbox or return
|
|
131
|
-
return unless comment.creative_id == inbox.id
|
|
132
|
-
|
|
133
|
-
pointer = Collavre::CommentReadPointer.find_or_initialize_by(user: current_user, creative: inbox)
|
|
134
|
-
new_id = [ pointer.last_read_comment_id || 0, comment.id ].max
|
|
135
|
-
pointer.update(last_read_comment_id: new_id)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def current_user_inbox
|
|
139
|
-
return @current_user_inbox if defined?(@current_user_inbox)
|
|
140
|
-
|
|
141
|
-
@current_user_inbox = Collavre::Creative.inbox_for(current_user)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Read the whole message aloud, but spoken: strip markdown link syntax to
|
|
145
|
-
# its text and collapse whitespace so TTS doesn't read raw URLs.
|
|
146
|
-
def speakable(text)
|
|
147
|
-
text.to_s.gsub(/\[([^\]]+)\]\([^)]*\)/, '\1').gsub(/\s+/, " ").strip
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def authorized_comment?(comment)
|
|
151
|
-
return true if comment.approver_id == current_user.id
|
|
152
|
-
return true if own_inbox_notice?(comment)
|
|
153
|
-
|
|
154
|
-
author = comment.user
|
|
155
|
-
author.respond_to?(:created_by_id) && agent_ids.include?(author.id)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# A system-authored notice in the caller's own Inbox#System topic. Owning
|
|
159
|
-
# the inbox is the authorization proof (the notice has no approver/author).
|
|
160
|
-
def own_inbox_notice?(comment)
|
|
161
|
-
inbox = current_user_inbox or return false
|
|
162
|
-
|
|
163
|
-
comment.user_id.nil? &&
|
|
164
|
-
comment.creative_id == inbox.id &&
|
|
165
|
-
comment.topic_id == inbox.system_topic.id
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# (A) bounded decision: spoken response IS the approve/deny button.
|
|
169
|
-
def decide_on(comment)
|
|
170
|
-
behavior = behavior_from(params[:response])
|
|
171
|
-
|
|
172
|
-
unless %w[allow deny].include?(behavior)
|
|
173
|
-
return render_speak(:clarify_decision, action: { type: "needs_clarification", comment_id: comment.id })
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
case decide_permission(comment, behavior)
|
|
177
|
-
when :unauthorized
|
|
178
|
-
render_speak(:not_authorized, action: { type: "not_authorized", comment_id: comment.id }, status: :forbidden)
|
|
179
|
-
when :already_decided
|
|
180
|
-
render_speak(:already_decided, action: { type: "already_decided", comment_id: comment.id })
|
|
181
|
-
else
|
|
182
|
-
render_speak(
|
|
183
|
-
behavior == "allow" ? :approved : :denied,
|
|
184
|
-
action: { type: behavior == "allow" ? "approved" : "denied", comment_id: comment.id }
|
|
185
|
-
)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# (B) free-form: relay the utterance verbatim as a human reply.
|
|
190
|
-
def relay_free_text(comment)
|
|
191
|
-
text = params[:response].to_s.strip
|
|
192
|
-
return render_speak(:empty_response, action: { type: "noop" }) if text.blank?
|
|
193
|
-
|
|
194
|
-
# A System-inbox notice is a stand-in for the origin comment it quotes;
|
|
195
|
-
# the reply must land on that origin thread (the System topic itself
|
|
196
|
-
# never dispatches AI), not on the notice.
|
|
197
|
-
target = comment.quoted_comment || comment
|
|
198
|
-
topic = target.topic
|
|
199
|
-
creative = topic&.creative&.effective_origin || target.creative
|
|
200
|
-
# quoted_comment threads the reply to the message being answered, but
|
|
201
|
-
# review_type: :question keeps review_message? false — otherwise the
|
|
202
|
-
# agent's response would update the quoted comment IN PLACE (review flow)
|
|
203
|
-
# instead of posting a new reply, so no new comment is created and the
|
|
204
|
-
# Inbox#System alarm never fires. Same guard as InboxReplyService (#1301).
|
|
205
|
-
reply = creative.comments.create!(
|
|
206
|
-
content: text, user: current_user, topic: topic,
|
|
207
|
-
quoted_comment: target, review_type: :question
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
render_speak(:relayed, action: { type: "relayed", comment_id: reply.id, topic_id: topic&.id })
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def behavior_from(response)
|
|
214
|
-
text = response.to_s.strip
|
|
215
|
-
return "allow" if text.match?(APPROVE)
|
|
216
|
-
return "deny" if text.match?(DENY)
|
|
217
|
-
|
|
218
|
-
nil
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Collavre
|
|
4
|
-
module Api
|
|
5
|
-
module V1
|
|
6
|
-
module Mobile
|
|
7
|
-
# Shared base for the voice-companion mobile API. Inherits Doorkeeper
|
|
8
|
-
# bearer auth from Api::V1::BaseController.
|
|
9
|
-
#
|
|
10
|
-
# ★ The API base controller authenticates the token but does NOT run the
|
|
11
|
-
# host app's locale around_action, so I18n would otherwise serve the
|
|
12
|
-
# process-default locale. Every persisted/spoken string here must match
|
|
13
|
-
# the speaker, so we wrap every action in I18n.with_locale (app-supplied
|
|
14
|
-
# `locale` param wins, else the user's stored locale).
|
|
15
|
-
class BaseController < Collavre::Api::V1::BaseController
|
|
16
|
-
around_action :with_request_locale
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def with_request_locale(&block)
|
|
21
|
-
I18n.with_locale(requested_locale, &block)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def requested_locale
|
|
25
|
-
normalize_locale(params[:locale].presence || current_user&.locale)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def normalize_locale(value)
|
|
29
|
-
return I18n.default_locale if value.blank?
|
|
30
|
-
|
|
31
|
-
base = value.to_s.tr("-", "_").split("_").first.to_sym
|
|
32
|
-
I18n.available_locales.include?(base) ? base : I18n.default_locale
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def summarizer
|
|
36
|
-
@summarizer ||= Collavre::Mobile::EventSummarizer.new(locale: I18n.locale)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# User-owned AI agents (Claude Channel sessions, etc.).
|
|
40
|
-
def agent_ids
|
|
41
|
-
@agent_ids ||= Collavre::User.ai_agents.where(created_by_id: current_user.id).pluck(:id)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Undecided permission prompts the token holder is the approver for.
|
|
45
|
-
def pending_approvals
|
|
46
|
-
Collavre::Comment.where(approver_id: current_user.id, action_executed_at: nil)
|
|
47
|
-
.where.not(action: nil)
|
|
48
|
-
.order(:created_at)
|
|
49
|
-
.limit(50)
|
|
50
|
-
.select(&:claude_channel_permission?)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def reply(key)
|
|
54
|
-
I18n.t("collavre.mobile.reply.#{key}")
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def label_for_topic(topic_id)
|
|
58
|
-
topic = topic_id && Collavre::Topic.find_by(id: topic_id)
|
|
59
|
-
topic&.name.presence || I18n.t("collavre.mobile.default_task_label")
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# List/display title for an event: "Creative#Topic" so the app can show
|
|
63
|
-
# which thread a message belongs to (spec: 제목을 크리에이티브#토픽 형태로).
|
|
64
|
-
def title_for_topic(topic_id)
|
|
65
|
-
topic = topic_id && Collavre::Topic.find_by(id: topic_id)
|
|
66
|
-
name = topic&.name.presence || I18n.t("collavre.mobile.default_task_label")
|
|
67
|
-
creative = topic&.creative&.effective_origin || topic&.creative
|
|
68
|
-
creative&.description.present? ? "#{creative.description}##{name}" : name
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def render_speak(reply_key_or_text, action: {}, speak: true, status: :ok)
|
|
72
|
-
text = reply_key_or_text.is_a?(Symbol) ? reply(reply_key_or_text) : reply_key_or_text
|
|
73
|
-
render json: { reply: text, speak: speak, action: action }, status: status
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Gate + decide + broadcast a Claude Channel permission. Routes the
|
|
77
|
-
# voice path through the same approval_status gate the web path uses
|
|
78
|
-
# (Comments::ApprovalActions). Without this the caller could decide a
|
|
79
|
-
# prompt they merely own the authoring agent for but are not the
|
|
80
|
-
# designated approver of (authorized_comment? lets agent-owned events
|
|
81
|
-
# through). Returns a symbol: :ok | :unauthorized | :already_decided.
|
|
82
|
-
def decide_permission(comment, behavior)
|
|
83
|
-
return :unauthorized unless comment.can_be_approved_by?(current_user)
|
|
84
|
-
|
|
85
|
-
comment.decide_claude_channel_permission!(behavior, by: current_user)
|
|
86
|
-
comment.broadcast_claude_channel_permission_decision(behavior)
|
|
87
|
-
:ok
|
|
88
|
-
rescue Collavre::Comment::ClaudeChannelPermission::AlreadyDecided
|
|
89
|
-
:already_decided
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Collavre
|
|
4
|
-
module Api
|
|
5
|
-
module V1
|
|
6
|
-
module Mobile
|
|
7
|
-
# FCM registration for the Android companion. Push is a follow-up (MVP
|
|
8
|
-
# uses foreground polling), but registering the device now keeps the
|
|
9
|
-
# token current so push can light up without a client change.
|
|
10
|
-
class DevicesController < BaseController
|
|
11
|
-
def create
|
|
12
|
-
device = Collavre::Device.find_by(fcm_token: device_params[:fcm_token]) ||
|
|
13
|
-
current_user.devices.find_or_initialize_by(client_id: device_params[:client_id])
|
|
14
|
-
|
|
15
|
-
device.assign_attributes(device_params)
|
|
16
|
-
device.user = current_user
|
|
17
|
-
device.device_type = :android if device.device_type.blank?
|
|
18
|
-
device.save!
|
|
19
|
-
head :no_content
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def device_params
|
|
25
|
-
params.require(:device).permit(:client_id, :device_type, :app_id, :app_version, :fcm_token)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Collavre
|
|
4
|
-
module Api
|
|
5
|
-
module V1
|
|
6
|
-
module Mobile
|
|
7
|
-
# A plain mic press (no message selected) routes here: the utterance
|
|
8
|
-
# starts a new piece of work in the user's Inbox#Main, exactly like
|
|
9
|
-
# typing in the inbox chat — dispatch then matches an agent via
|
|
10
|
-
# routing_expression. Replies to a SPECIFIC message go through
|
|
11
|
-
# agent_events#respond instead (they carry the target event id).
|
|
12
|
-
class VoiceCommandsController < BaseController
|
|
13
|
-
def create
|
|
14
|
-
text = params[:text].to_s.strip
|
|
15
|
-
return render_speak(:empty_response, action: { type: "noop" }) if text.blank?
|
|
16
|
-
|
|
17
|
-
inbox = Collavre::Creative.inbox_for(current_user)
|
|
18
|
-
comment = inbox.comments.create!(content: text, user: current_user)
|
|
19
|
-
render_speak(:sent, action: { type: "message_sent", comment_id: comment.id })
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
module Collavre
|
|
2
|
-
class InboxItemsController < ApplicationController
|
|
3
|
-
before_action :set_inbox_item, only: [ :update, :destroy ]
|
|
4
|
-
|
|
5
|
-
PER_PAGE = 20
|
|
6
|
-
|
|
7
|
-
def index
|
|
8
|
-
scope = InboxItem.where(owner: Current.user).order(created_at: :desc)
|
|
9
|
-
scope = scope.new_items unless params[:show] == "all"
|
|
10
|
-
|
|
11
|
-
per_page = params[:per_page].presence&.to_i || PER_PAGE
|
|
12
|
-
per_page = PER_PAGE if per_page <= 0 || per_page > 100
|
|
13
|
-
page = params[:page].presence&.to_i || 1
|
|
14
|
-
page = 1 if page < 1
|
|
15
|
-
offset = (page - 1) * per_page
|
|
16
|
-
|
|
17
|
-
items = scope.offset(offset).limit(per_page + 1).to_a
|
|
18
|
-
@inbox_items = items.first(per_page)
|
|
19
|
-
@next_page = items.length > per_page ? page + 1 : nil
|
|
20
|
-
|
|
21
|
-
respond_to do |format|
|
|
22
|
-
format.html do
|
|
23
|
-
render partial: "collavre/inbox_items/list", locals: { items: @inbox_items, next_page: @next_page }
|
|
24
|
-
end
|
|
25
|
-
format.json do
|
|
26
|
-
render json: {
|
|
27
|
-
items_html: render_to_string(partial: "collavre/inbox_items/items", formats: [ :html ], locals: { items: @inbox_items }),
|
|
28
|
-
next_page: @next_page,
|
|
29
|
-
empty: @inbox_items.empty?
|
|
30
|
-
}
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def count
|
|
36
|
-
c = InboxItem.where(owner: Current.user, state: "new").count
|
|
37
|
-
render json: { count: c }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def update
|
|
41
|
-
unless @inbox_item.owner == Current.user
|
|
42
|
-
head :forbidden
|
|
43
|
-
return
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
unless InboxItem.states.key?(params[:state].to_s)
|
|
47
|
-
head :unprocessable_entity
|
|
48
|
-
return
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
@inbox_item.update(state: params[:state])
|
|
52
|
-
head :no_content
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def destroy
|
|
56
|
-
if @inbox_item.owner == Current.user
|
|
57
|
-
@inbox_item.destroy
|
|
58
|
-
head :no_content
|
|
59
|
-
else
|
|
60
|
-
head :forbidden
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def set_inbox_item
|
|
67
|
-
@inbox_item = InboxItem.where(owner: Current.user).find(params[:id])
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
module Collavre
|
|
2
|
-
# Session-authenticated endpoint backing the inline typo-correction UI. The
|
|
3
|
-
# frontend also gates by device/location, but we re-check here (fail closed)
|
|
4
|
-
# before spending an LLM call, and echo the user's auto-apply threshold so the
|
|
5
|
-
# client never has to hardcode it.
|
|
6
|
-
class TypoCorrectionsController < ApplicationController
|
|
7
|
-
# The client debounce is only a hint — a scripted/compromised page can loop
|
|
8
|
-
# this endpoint with arbitrary text, and every accepted call spends an LLM
|
|
9
|
-
# request. Bound both the call rate and the per-call size server-side. The
|
|
10
|
-
# chat composer is short text; anything larger is not a typo-correction case.
|
|
11
|
-
MAX_TEXT_LENGTH = 4_000
|
|
12
|
-
RATE_LIMIT = 120 # requests
|
|
13
|
-
RATE_PERIOD = 1.minute # ...per user per minute
|
|
14
|
-
|
|
15
|
-
def create
|
|
16
|
-
user = Current.user
|
|
17
|
-
device = params[:device].to_s
|
|
18
|
-
location = params[:location].presence || "chat"
|
|
19
|
-
text = params[:text].to_s
|
|
20
|
-
|
|
21
|
-
unless user.typo_correction_active_for?(device: device, location: location)
|
|
22
|
-
return render json: { edits: [], threshold: user.typo_correction_threshold }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Oversized input: skip the LLM entirely (return no edits, not an error —
|
|
26
|
-
# this is a background affordance, not a user-facing failure).
|
|
27
|
-
if text.length > MAX_TEXT_LENGTH
|
|
28
|
-
return render json: { edits: [], threshold: user.typo_correction_threshold }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Raises RateLimitExceeded -> 429 (DynamicRateLimiting), which the client
|
|
32
|
-
# treats as "no suggestions this round".
|
|
33
|
-
check_rate_limit!(key: "typo_correction:#{user.id}", limit: RATE_LIMIT, period: RATE_PERIOD)
|
|
34
|
-
|
|
35
|
-
edits = TypoCorrector.new.correct(text)
|
|
36
|
-
render json: { edits: edits, threshold: user.typo_correction_threshold }
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
detectDevice,
|
|
3
|
-
shouldRun,
|
|
4
|
-
buildCandidateList,
|
|
5
|
-
anchorEdits,
|
|
6
|
-
applyEditAt,
|
|
7
|
-
shiftEditsAfter,
|
|
8
|
-
partitionByThreshold,
|
|
9
|
-
DEVICE_VOICE,
|
|
10
|
-
DEVICE_SOFT_KEYBOARD,
|
|
11
|
-
DEVICE_PHYSICAL_KEYBOARD,
|
|
12
|
-
} from '../typo_correction'
|
|
13
|
-
|
|
14
|
-
const settings = ({
|
|
15
|
-
enabled = true,
|
|
16
|
-
onVoice = true,
|
|
17
|
-
onSoftKeyboard = true,
|
|
18
|
-
onPhysicalKeyboard = false,
|
|
19
|
-
inChat = true,
|
|
20
|
-
inEditor = false,
|
|
21
|
-
} = {}) => ({ enabled, onVoice, onSoftKeyboard, onPhysicalKeyboard, inChat, inEditor })
|
|
22
|
-
|
|
23
|
-
describe('detectDevice', () => {
|
|
24
|
-
test('voice recognition wins regardless of keyboard signals', () => {
|
|
25
|
-
expect(detectDevice({ voiceActive: true, lastPrintableKeydownAt: 100, inputAt: 110 })).toBe(DEVICE_VOICE)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('printable keydown right before input => physical keyboard', () => {
|
|
29
|
-
expect(detectDevice({ lastPrintableKeydownAt: 1000, inputAt: 1005 })).toBe(DEVICE_PHYSICAL_KEYBOARD)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('input with no preceding printable keydown => soft keyboard', () => {
|
|
33
|
-
expect(detectDevice({ lastPrintableKeydownAt: null, inputAt: 1000 })).toBe(DEVICE_SOFT_KEYBOARD)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('stale keydown (too far before input) => soft keyboard', () => {
|
|
37
|
-
expect(detectDevice({ lastPrintableKeydownAt: 1000, inputAt: 2000 })).toBe(DEVICE_SOFT_KEYBOARD)
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('shouldRun (2D gating)', () => {
|
|
42
|
-
test('runs when master + device + location all on', () => {
|
|
43
|
-
expect(shouldRun(settings(), { device: DEVICE_SOFT_KEYBOARD, location: 'chat' })).toBe(true)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('blocked when master off', () => {
|
|
47
|
-
expect(shouldRun(settings({ enabled: false }), { device: DEVICE_SOFT_KEYBOARD, location: 'chat' })).toBe(false)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('physical keyboard off by default blocks even in chat', () => {
|
|
51
|
-
expect(shouldRun(settings(), { device: DEVICE_PHYSICAL_KEYBOARD, location: 'chat' })).toBe(false)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('editor location off by default blocks even with soft keyboard', () => {
|
|
55
|
-
expect(shouldRun(settings(), { device: DEVICE_SOFT_KEYBOARD, location: 'editor' })).toBe(false)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('voice in chat runs', () => {
|
|
59
|
-
expect(shouldRun(settings(), { device: DEVICE_VOICE, location: 'chat' })).toBe(true)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('null settings never runs', () => {
|
|
63
|
-
expect(shouldRun(null, { device: DEVICE_VOICE, location: 'chat' })).toBe(false)
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('buildCandidateList', () => {
|
|
68
|
-
test('candidate state: original pre-filled, suggestions by confidence', () => {
|
|
69
|
-
const list = buildCandidateList({
|
|
70
|
-
currentValue: '안녀하세요',
|
|
71
|
-
originalWord: '안녀하세요',
|
|
72
|
-
suggestions: [
|
|
73
|
-
{ suggestion: '안녕하세요', confidence: 0.95 },
|
|
74
|
-
{ suggestion: '안녕하셔요', confidence: 0.4 },
|
|
75
|
-
],
|
|
76
|
-
})
|
|
77
|
-
expect(list.map((i) => i.value)).toEqual(['안녀하세요', '안녕하세요', '안녕하셔요'])
|
|
78
|
-
expect(list[0].isCurrent).toBe(true)
|
|
79
|
-
expect(list[0].role).toBe('original')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('auto-applied state: corrected word current, original offered as undo', () => {
|
|
83
|
-
const list = buildCandidateList({
|
|
84
|
-
currentValue: '있습니다',
|
|
85
|
-
originalWord: '잇습니다',
|
|
86
|
-
suggestions: [{ suggestion: '있습니다', confidence: 0.99 }],
|
|
87
|
-
})
|
|
88
|
-
// current (applied) first, then original (undo); suggestion dupes current and is dropped.
|
|
89
|
-
expect(list.map((i) => i.value)).toEqual(['있습니다', '잇습니다'])
|
|
90
|
-
expect(list[0].role).toBe('applied')
|
|
91
|
-
expect(list[0].isCurrent).toBe(true)
|
|
92
|
-
expect(list[1].role).toBe('original')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('de-duplicates and skips empties', () => {
|
|
96
|
-
const list = buildCandidateList({
|
|
97
|
-
currentValue: 'a',
|
|
98
|
-
originalWord: 'a',
|
|
99
|
-
suggestions: [{ suggestion: 'a' }, { suggestion: '' }, { suggestion: 'b', confidence: 0.5 }],
|
|
100
|
-
})
|
|
101
|
-
expect(list.map((i) => i.value)).toEqual(['a', 'b'])
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
describe('anchorEdits', () => {
|
|
106
|
-
test('binds offsets to substrings', () => {
|
|
107
|
-
const text = '안녀하세요 저는 콜라브를 만들고 잇습니다'
|
|
108
|
-
const edits = anchorEdits(text, [
|
|
109
|
-
{ original: '안녀하세요', suggestion: '안녕하세요' },
|
|
110
|
-
{ original: '잇습니다', suggestion: '있습니다' },
|
|
111
|
-
])
|
|
112
|
-
expect(text.slice(edits[0].start, edits[0].end)).toBe('안녀하세요')
|
|
113
|
-
expect(text.slice(edits[1].start, edits[1].end)).toBe('잇습니다')
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('repeated words anchor to distinct occurrences, not all to the first', () => {
|
|
117
|
-
const text = 'teh cat and teh dog'
|
|
118
|
-
const edits = anchorEdits(text, [
|
|
119
|
-
{ original: 'teh', suggestion: 'the' },
|
|
120
|
-
{ original: 'teh', suggestion: 'the' },
|
|
121
|
-
])
|
|
122
|
-
expect(edits[0].start).toBe(0)
|
|
123
|
-
expect(edits[1].start).toBe(12)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
test('drops edits whose original is gone', () => {
|
|
127
|
-
const edits = anchorEdits('hello world', [{ original: 'xyz', suggestion: 'abc' }])
|
|
128
|
-
expect(edits).toEqual([])
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('honors the server offset so a duplicate inside a protected span is skipped', () => {
|
|
132
|
-
// "teh" appears first inside backticks (a region the server excludes) and
|
|
133
|
-
// again in plain text at offset 15. A plain search would bind the protected
|
|
134
|
-
// one; the offset pins us to the valid occurrence.
|
|
135
|
-
const text = 'use `teh` then teh'
|
|
136
|
-
const edits = anchorEdits(text, [{ original: 'teh', suggestion: 'the', offset: 15 }])
|
|
137
|
-
expect(edits[0].start).toBe(15)
|
|
138
|
-
expect(text.slice(edits[0].start, edits[0].end)).toBe('teh')
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
test('falls back to search when the offset no longer matches the text', () => {
|
|
142
|
-
const text = 'hello teh'
|
|
143
|
-
const edits = anchorEdits(text, [{ original: 'teh', suggestion: 'the', offset: 99 }])
|
|
144
|
-
expect(edits[0].start).toBe(6)
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe('applyEditAt + shiftEditsAfter (re-anchoring)', () => {
|
|
149
|
-
test('applying one edit shifts the offsets of later edits', () => {
|
|
150
|
-
let text = 'teh cat saw teh dog'
|
|
151
|
-
const edits = anchorEdits(text, [
|
|
152
|
-
{ original: 'teh', suggestion: 'the' },
|
|
153
|
-
{ original: 'teh', suggestion: 'the' },
|
|
154
|
-
])
|
|
155
|
-
const first = edits[0]
|
|
156
|
-
const { text: t2, delta } = applyEditAt(text, { start: first.start, end: first.end, replacement: 'the' })
|
|
157
|
-
text = t2
|
|
158
|
-
expect(text).toBe('the cat saw teh dog')
|
|
159
|
-
const remaining = shiftEditsAfter(edits.slice(1), first.start, delta)
|
|
160
|
-
// 'the' and 'teh' are the same length here, delta 0; offset unchanged & still valid.
|
|
161
|
-
expect(text.slice(remaining[0].start, remaining[0].end)).toBe('teh')
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('length-changing replacement re-anchors later edits correctly', () => {
|
|
165
|
-
let text = 'a wrng word and anothr'
|
|
166
|
-
const edits = anchorEdits(text, [
|
|
167
|
-
{ original: 'wrng', suggestion: 'wrong' },
|
|
168
|
-
{ original: 'anothr', suggestion: 'another' },
|
|
169
|
-
])
|
|
170
|
-
const first = edits[0]
|
|
171
|
-
const { text: t2, delta } = applyEditAt(text, { start: first.start, end: first.end, replacement: 'wrong' })
|
|
172
|
-
text = t2
|
|
173
|
-
expect(text).toBe('a wrong word and anothr')
|
|
174
|
-
const remaining = shiftEditsAfter(edits.slice(1), first.start, delta)
|
|
175
|
-
expect(text.slice(remaining[0].start, remaining[0].end)).toBe('anothr')
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
describe('partitionByThreshold', () => {
|
|
180
|
-
test('splits at/above vs below threshold', () => {
|
|
181
|
-
const { autoApplied, candidates } = partitionByThreshold(
|
|
182
|
-
[
|
|
183
|
-
{ original: 'a', confidence: 0.9 },
|
|
184
|
-
{ original: 'b', confidence: 0.8 },
|
|
185
|
-
{ original: 'c', confidence: 0.5 },
|
|
186
|
-
],
|
|
187
|
-
80,
|
|
188
|
-
)
|
|
189
|
-
expect(autoApplied.map((e) => e.original)).toEqual(['a', 'b'])
|
|
190
|
-
expect(candidates.map((e) => e.original)).toEqual(['c'])
|
|
191
|
-
})
|
|
192
|
-
})
|