From c08fa631a9667a2fa9680ea64ebd4f1bfb2095f2 Mon Sep 17 00:00:00 2001 From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:07:00 +0530 Subject: [PATCH 1/5] feat: Add temporary account setting to disable Captain auto-resolve (#13680) Add a temporary `captain_disable_auto_resolve` boolean setting on accounts to prevent Captain from resolving conversations. Guards both the scheduled resolution job and the assistant's resolve tool. --------- Co-authored-by: Claude Opus 4.6 --- app/models/account.rb | 2 ++ ...inbox_pending_conversations_resolution_job.rb | 2 ++ .../conversations_resolution_scheduler_job.rb | 1 + .../captain/tools/resolve_conversation_tool.rb | 1 + ..._pending_conversations_resolution_job_spec.rb | 11 +++++++++++ ...onversations_resolution_scheduler_job_spec.rb | 16 ++++++++++++++++ .../tools/resolve_conversation_tool_spec.rb | 11 +++++++++++ 7 files changed, 44 insertions(+) diff --git a/app/models/account.rb b/app/models/account.rb index 4816494fb..eabaa5c26 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -41,6 +41,7 @@ class Account < ApplicationRecord 'audio_transcriptions': { 'type': %w[boolean null] }, 'auto_resolve_label': { 'type': %w[string null] }, 'keep_pending_on_bot_failure': { 'type': %w[boolean null] }, + 'captain_disable_auto_resolve': { 'type': %w[boolean null] }, 'conversation_required_attributes': { 'type': %w[array null], 'items': { 'type': 'string' } @@ -90,6 +91,7 @@ class Account < ApplicationRecord store_accessor :settings, :audio_transcriptions, :auto_resolve_label store_accessor :settings, :captain_models, :captain_features store_accessor :settings, :keep_pending_on_bot_failure + store_accessor :settings, :captain_disable_auto_resolve has_many :account_users, dependent: :destroy_async has_many :agent_bot_inboxes, dependent: :destroy_async diff --git a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb index d3f1f5d96..ab9ca2ab1 100644 --- a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb +++ b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb @@ -2,6 +2,8 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob queue_as :low def perform(inbox) + return if inbox.account.captain_disable_auto_resolve + Current.executed_by = inbox.captain_assistant resolvable_conversations = inbox.conversations.pending.where('last_activity_at < ? ', Time.now.utc - 1.hour).limit(Limits::BULK_ACTIONS_LIMIT) diff --git a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb index 599dee96a..8b6527c93 100644 --- a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb +++ b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb @@ -12,6 +12,7 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob inbox = captain_inbox.inbox next if inbox.email? + next if inbox.account.captain_disable_auto_resolve Captain::InboxPendingConversationsResolutionJob.perform_later( inbox diff --git a/enterprise/lib/captain/tools/resolve_conversation_tool.rb b/enterprise/lib/captain/tools/resolve_conversation_tool.rb index 0d2563a8b..5d96d3af1 100644 --- a/enterprise/lib/captain/tools/resolve_conversation_tool.rb +++ b/enterprise/lib/captain/tools/resolve_conversation_tool.rb @@ -6,6 +6,7 @@ class Captain::Tools::ResolveConversationTool < Captain::Tools::BasePublicTool conversation = find_conversation(tool_context.state) return 'Conversation not found' unless conversation return "Conversation ##{conversation.display_id} is already resolved" if conversation.resolved? + return 'Auto-resolve is disabled for this account' if conversation.account.captain_disable_auto_resolve log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason }) diff --git a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb index 1a8a5a342..ab8f0296c 100644 --- a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb +++ b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb @@ -64,4 +64,15 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do } ) end + + it 'does not resolve conversations when auto-resolve is disabled at execution time' do + inbox.account.update!(captain_disable_auto_resolve: true) + + expect do + described_class.perform_now(inbox) + end.not_to(change { resolvable_pending_conversation.reload.status }) + + expect(resolvable_pending_conversation.reload.status).to eq('pending') + expect(resolvable_pending_conversation.messages.outgoing).to be_empty + end end diff --git a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb index b67877412..343100a50 100644 --- a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb +++ b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb @@ -30,6 +30,22 @@ RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do end end + context 'when account has captain_disable_auto_resolve enabled' do + let!(:regular_inbox) { create(:inbox, account: account) } + + before do + create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox) + account.update!(captain_disable_auto_resolve: true) + end + + it 'does not enqueue resolution jobs' do + expect do + described_class.perform_now + end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob) + .with(regular_inbox) + end + end + context 'when inbox has no captain enabled' do let!(:inbox_without_captain) { create(:inbox, account: create(:account)) } diff --git a/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb b/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb index f91f430e8..d5792cf78 100644 --- a/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb +++ b/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb @@ -36,6 +36,17 @@ RSpec.describe Captain::Tools::ResolveConversationTool do end end + describe 'when auto-resolve is disabled for the account' do + before { account.update!(captain_disable_auto_resolve: true) } + + it 'does not resolve and returns a disabled message' do + result = tool.perform(tool_context, reason: 'Possible spam') + + expect(result).to eq('Auto-resolve is disabled for this account') + expect(conversation.reload).not_to be_resolved + end + end + describe 'resolving an already resolved conversation' do let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :resolved) } From 8d48e05283b813c5ed0429455ad8f41a8e1b77ed Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 2 Mar 2026 13:12:42 +0530 Subject: [PATCH 2/5] feat: reclaim `mobile_v2` flag for `report_rollup` (#13666) --- config/features.yml | 5 ++--- ...260226153427_disable_report_rollup_for_all_accounts.rb | 8 ++++++++ db/schema.rb | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb diff --git a/config/features.yml b/config/features.yml index 65b3c6194..eed6a9da1 100644 --- a/config/features.yml +++ b/config/features.yml @@ -74,10 +74,9 @@ - name: voice_recorder display_name: Voice Recorder enabled: true -- name: mobile_v2 - display_name: Mobile App V2 +- name: report_rollup + display_name: Report Rollup enabled: false - deprecated: true - name: channel_website display_name: Website Channel enabled: true diff --git a/db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb b/db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb new file mode 100644 index 000000000..60a8f4604 --- /dev/null +++ b/db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb @@ -0,0 +1,8 @@ +class DisableReportRollupForAllAccounts < ActiveRecord::Migration[7.1] + def up + Account.feature_report_rollup.find_each(batch_size: 100) do |account| + account.disable_features(:report_rollup) + account.save!(validate: false) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8a450e734..4bb0ca3af 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_02_26_084618) do +ActiveRecord::Schema[7.1].define(version: 2026_02_26_153427) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" From ab93821d2b210d60d7cd6b0cd1ff0410458c2cf7 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 2 Mar 2026 02:18:29 -0800 Subject: [PATCH 3/5] fix(agent-bot): stabilize webhook delivery for transient upstream failures (#13521) This fixes the agent-bot webhook delivery path so transient upstream failures follow the expected delivery lifecycle. Existing fallback behavior is preserved, and fallback actions are applied only after delivery attempts are exhausted. To reproduce, configure an agent-bot webhook endpoint to return 429/500 for message events. Before this fix, failure handling could be applied too early; after this fix, delivery attempts complete first and then existing fallback handling runs. Tested with: - bundle exec rspec spec/jobs/agent_bots/webhook_job_spec.rb spec/lib/webhooks/trigger_spec.rb - bundle exec rubocop spec/jobs/agent_bots/webhook_job_spec.rb spec/lib/webhooks/trigger_spec.rb --------- Co-authored-by: Muhsin Keloth --- app/jobs/agent_bots/webhook_job.rb | 7 ++++ lib/webhooks/trigger.rb | 12 +++++- spec/jobs/agent_bots/webhook_job_spec.rb | 29 ++++++++++++++ spec/lib/webhooks/trigger_spec.rb | 50 ++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/app/jobs/agent_bots/webhook_job.rb b/app/jobs/agent_bots/webhook_job.rb index b3a3d6cc1..2786ce70e 100644 --- a/app/jobs/agent_bots/webhook_job.rb +++ b/app/jobs/agent_bots/webhook_job.rb @@ -1,7 +1,14 @@ class AgentBots::WebhookJob < WebhookJob queue_as :high + retry_on RestClient::TooManyRequests, RestClient::InternalServerError, wait: 3.seconds, attempts: 3 do |job, error| + url, payload, webhook_type = job.arguments + Webhooks::Trigger.new(url, payload, webhook_type || :agent_bot_webhook).handle_failure(error) + end def perform(url, payload, webhook_type = :agent_bot_webhook) super(url, payload, webhook_type) + rescue RestClient::TooManyRequests, RestClient::InternalServerError => e + Rails.logger.warn("[AgentBots::WebhookJob] attempt #{executions} failed #{e.class.name}") + raise end end diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index 456e186ca..7cb15c836 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -15,9 +15,17 @@ class Webhooks::Trigger def execute perform_request + rescue RestClient::TooManyRequests, RestClient::InternalServerError => e + raise if @webhook_type == :agent_bot_webhook + + handle_failure(e) rescue StandardError => e - handle_error(e) - Rails.logger.warn "Exception: Invalid webhook URL #{@url} : #{e.message}" + handle_failure(e) + end + + def handle_failure(error) + handle_error(error) + Rails.logger.warn "Exception: Invalid webhook URL #{@url} : #{error.message}" end private diff --git a/spec/jobs/agent_bots/webhook_job_spec.rb b/spec/jobs/agent_bots/webhook_job_spec.rb index 346d85e83..c14c46cb3 100644 --- a/spec/jobs/agent_bots/webhook_job_spec.rb +++ b/spec/jobs/agent_bots/webhook_job_spec.rb @@ -8,6 +8,16 @@ RSpec.describe AgentBots::WebhookJob do let(:url) { 'https://test.com' } let(:payload) { { name: 'test' } } let(:webhook_type) { :agent_bot_webhook } + let(:retryable_error) { RestClient::InternalServerError.new(nil, 500) } + + before do + ActiveJob::Base.queue_adapter = :test + end + + after do + clear_enqueued_jobs + clear_performed_jobs + end it 'queues the job' do expect { job }.to have_enqueued_job(described_class) @@ -19,4 +29,23 @@ RSpec.describe AgentBots::WebhookJob do expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type, secret: nil, delivery_id: nil) perform_enqueued_jobs { job } end + + it 'configures retry handlers for 429 and 500 errors' do + handlers = described_class.rescue_handlers.map(&:first) + + expect(handlers).to include('RestClient::TooManyRequests', 'RestClient::InternalServerError') + end + + it 'retries 3 times and handles failure after retries are exhausted' do + allow(Webhooks::Trigger).to receive(:execute).and_raise(retryable_error) + trigger_instance = instance_double(Webhooks::Trigger, handle_failure: true) + allow(Webhooks::Trigger).to receive(:new).and_return(trigger_instance) + allow(Rails.logger).to receive(:warn) + + expect(Webhooks::Trigger).to receive(:execute).exactly(3).times + expect(trigger_instance).to receive(:handle_failure).with(instance_of(RestClient::InternalServerError)).once + expect(Rails.logger).to receive(:warn).with(/AgentBots::WebhookJob/).exactly(3).times + + perform_enqueued_jobs { job } + end end diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb index 1e047b557..90d1ce7f8 100644 --- a/spec/lib/webhooks/trigger_spec.rb +++ b/spec/lib/webhooks/trigger_spec.rb @@ -77,6 +77,40 @@ describe Webhooks::Trigger do let!(:pending_conversation) { create(:conversation, inbox: inbox, status: :pending, account: account) } let!(:pending_message) { create(:message, account: account, inbox: inbox, conversation: pending_conversation) } + it 'raises 500 errors for retry and does not reopen conversation immediately' do + payload = { event: 'message_created', id: pending_message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: webhook_timeout + ).and_raise(RestClient::InternalServerError.new(nil, 500)).once + + expect { trigger.execute(url, payload, webhook_type) }.to raise_error(RestClient::InternalServerError) + expect(pending_conversation.reload.status).to eq('pending') + expect(Conversations::ActivityMessageJob).not_to have_been_enqueued + end + + it 'raises 429 errors for retry and does not reopen conversation immediately' do + payload = { event: 'message_created', id: pending_message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: webhook_timeout + ).and_raise(RestClient::TooManyRequests.new(nil, 429)).once + + expect { trigger.execute(url, payload, webhook_type) }.to raise_error(RestClient::TooManyRequests) + expect(pending_conversation.reload.status).to eq('pending') + expect(Conversations::ActivityMessageJob).not_to have_been_enqueued + end + it 'reopens conversation and enqueues activity message if pending' do payload = { event: 'message_created', id: pending_message.id } @@ -166,6 +200,22 @@ describe Webhooks::Trigger do expect(activity_message.content).to eq(agent_bot_error_content) end end + + it 'handles 500 without raising for non-agent webhooks' do + payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: webhook_timeout + ).and_raise(RestClient::InternalServerError.new(nil, 500)).once + + expect { trigger.execute(url, payload, webhook_type) }.not_to raise_error + expect(message.reload.status).to eq('failed') + end end describe 'request headers' do From 9aacc0335b8513cb4b1f297d14a1de81711ad5c9 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 2 Mar 2026 15:32:59 +0400 Subject: [PATCH 4/5] feat(facebook): use `HUMAN_AGENT` tag for Messenger replies when human-agent config is enabled (#13690) This PR updates Facebook Messenger outbound tagging in Chatwoot to support Human Agent messaging when enabled. Previously, Facebook outbound text and attachment messages were always sent with: ``` messaging_type: MESSAGE_TAG tag: ACCOUNT_UPDATE ``` With this change, the tag is selected dynamically: ``` HUMAN_AGENT when ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT is enabled ACCOUNT_UPDATE as fallback when the flag is disabled ``` --- app/services/facebook/send_on_facebook_service.rb | 8 ++++++-- .../facebook/send_on_facebook_service_spec.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/services/facebook/send_on_facebook_service.rb b/app/services/facebook/send_on_facebook_service.rb index ed3b7e4ab..baf72ef6e 100644 --- a/app/services/facebook/send_on_facebook_service.rb +++ b/app/services/facebook/send_on_facebook_service.rb @@ -49,7 +49,7 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService recipient: { id: contact.get_source_id(inbox.id) }, message: fb_text_message_payload, messaging_type: 'MESSAGE_TAG', - tag: 'ACCOUNT_UPDATE' + tag: message_tag } end @@ -90,10 +90,14 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService } }, messaging_type: 'MESSAGE_TAG', - tag: 'ACCOUNT_UPDATE' + tag: message_tag } end + def message_tag + @message_tag ||= GlobalConfigService.load('ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT', nil) ? 'HUMAN_AGENT' : 'ACCOUNT_UPDATE' + end + def attachment_type(attachment) return attachment.file_type if %w[image audio video file].include? attachment.file_type diff --git a/spec/services/facebook/send_on_facebook_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb index 4d5f9babd..f99b1c469 100644 --- a/spec/services/facebook/send_on_facebook_service_spec.rb +++ b/spec/services/facebook/send_on_facebook_service_spec.rb @@ -7,6 +7,7 @@ describe Facebook::SendOnFacebookService do allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true) allow(bot).to receive(:deliver).and_return({ recipient_id: '1008372609250235', message_id: 'mid.1456970487936:c34767dfe57ee6e339' }.to_json) create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation) + GlobalConfig.clear_cache end let!(:account) { create(:account) } @@ -90,6 +91,17 @@ describe Facebook::SendOnFacebookService do }, { page_id: facebook_channel.page_id }) end + it 'sends with HUMAN_AGENT tag when ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT is enabled' do + with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do + message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) + described_class.new(message: message).perform + expect(bot).to have_received(:deliver).with( + hash_including(tag: 'HUMAN_AGENT'), + { page_id: facebook_channel.page_id } + ) + end + end + it 'if message is sent with multiple attachments' do message = build(:message, content: nil, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) avatar = message.attachments.new(account_id: message.account_id, file_type: :image) From 89da4a2292386b5a8bc9680752ba9df0a14ddc1d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:27:51 +0530 Subject: [PATCH 5/5] feat: compose form improvements (#13668) --- app/javascript/dashboard/api/contacts.js | 4 +- .../dashboard/api/specs/contacts.spec.js | 14 ++- .../components-next/Editor/Editor.vue | 5 +- .../NewConversation/ComposeConversation.vue | 14 ++- .../components/ComposeNewConversationForm.vue | 49 ++++++++- .../components/EmailOptions.vue | 2 - .../components/InboxEmptyState.vue | 8 +- .../components/InboxSelector.vue | 7 ++ .../components/MessageEditor.vue | 82 ++++++++++---- .../helpers/composeConversationHelper.js | 47 ++++++-- .../specs/composeConversationHelper.spec.js | 104 ++++++++++++++++-- .../components-next/taginput/TagInput.vue | 2 +- .../widgets/WootWriter/CopilotMenuBar.vue | 37 +++++-- .../components/widgets/WootWriter/Editor.vue | 23 +++- .../widgets/WootWriter/ReplyTopPanel.vue | 10 +- .../widgets/conversation/ReplyBox.vue | 1 + .../dashboard/i18n/locale/en/contact.json | 6 +- .../components/SearchContactAgentSelector.vue | 12 +- 18 files changed, 354 insertions(+), 73 deletions(-) diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 1e76ac987..bae5623a7 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -57,14 +57,14 @@ class ContactAPI extends ApiClient { return axios.post(`${this.url}/${contactId}/labels`, { labels }); } - search(search = '', page = 1, sortAttr = 'name', label = '') { + search(search = '', page = 1, sortAttr = 'name', label = '', options = {}) { let requestURL = `${this.url}/search?${buildContactParams( page, sortAttr, label, search )}`; - return axios.get(requestURL); + return axios.get(requestURL, { signal: options.signal }); } active(page = 1, sortAttr = 'name') { diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index 0059518b0..b21aeb102 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -68,7 +68,19 @@ describe('#ContactsAPI', () => { it('#search', () => { contactAPI.search('leads', 1, 'date', 'customer-support'); expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support' + '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support', + { signal: undefined } + ); + }); + + it('#search with signal', () => { + const controller = new AbortController(); + contactAPI.search('leads', 1, 'date', 'customer-support', { + signal: controller.signal, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support', + { signal: controller.signal } ); }); diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue index c2cde6d17..847bbd600 100644 --- a/app/javascript/dashboard/components-next/Editor/Editor.vue +++ b/app/javascript/dashboard/components-next/Editor/Editor.vue @@ -28,7 +28,7 @@ const props = defineProps({ medium: { type: String, default: '' }, }); -const emit = defineEmits(['update:modelValue']); +const emit = defineEmits(['update:modelValue', 'executeCopilotAction']); const slots = useSlots(); @@ -113,6 +113,9 @@ watch( @input="handleInput" @focus="handleFocus" @blur="handleBlur" + @execute-copilot-action=" + (...args) => emit('executeCopilotAction', ...args) + " />
{ contact = rest; } selectedContact.value = contact; + contacts.value = []; if (contact?.id) { isFetchingInboxes.value = true; try { diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue index 0ed22ad0c..455abf996 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue @@ -15,6 +15,9 @@ import { prepareWhatsAppMessagePayload, } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper.js'; +import { useCopilotReply } from 'dashboard/composables/useCopilotReply'; +import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; + import ContactSelector from './ContactSelector.vue'; import InboxSelector from './InboxSelector.vue'; import EmailOptions from './EmailOptions.vue'; @@ -22,6 +25,7 @@ import MessageEditor from './MessageEditor.vue'; import ActionButtons from './ActionButtons.vue'; import InboxEmptyState from './InboxEmptyState.vue'; import AttachmentPreviews from './AttachmentPreviews.vue'; +import CopilotReplyBottomPanel from 'dashboard/components/widgets/WootWriter/CopilotReplyBottomPanel.vue'; const props = defineProps({ contacts: { type: Array, default: () => [] }, @@ -42,6 +46,7 @@ const props = defineProps({ const emit = defineEmits([ 'searchContacts', + 'resetContactSearch', 'discard', 'updateSelectedContact', 'updateTargetInbox', @@ -51,6 +56,8 @@ const emit = defineEmits([ const DEFAULT_FORMATTING = 'Context::Default'; +const copilot = useCopilotReply(); + const showContactsDropdown = ref(false); const showInboxesDropdown = ref(false); const showCcEmailsDropdown = ref(false); @@ -157,7 +164,7 @@ const isAnyDropdownActive = computed(() => { }); const handleContactSearch = value => { - showContactsDropdown.value = true; + showContactsDropdown.value = value.trim().length > 1; emit('searchContacts', value); }; @@ -172,12 +179,16 @@ const handleDropdownUpdate = (type, value) => { }; const searchCcEmails = value => { - showCcEmailsDropdown.value = true; + showBccEmailsDropdown.value = false; + emit('resetContactSearch'); + showCcEmailsDropdown.value = value.trim().length >= 2; emit('searchContacts', value); }; const searchBccEmails = value => { - showBccEmailsDropdown.value = true; + showCcEmailsDropdown.value = false; + emit('resetContactSearch'); + showBccEmailsDropdown.value = value.trim().length >= 2; emit('searchContacts', value); }; @@ -196,6 +207,7 @@ const stripMessageFormatting = channelType => { const handleInboxAction = ({ value, action, channelType, medium, ...rest }) => { v$.value.$reset(); + copilot.reset(false); // Strip unsupported formatting when changing the target inbox if (channelType) { @@ -222,6 +234,7 @@ const removeSignatureFromMessage = () => { const removeTargetInbox = value => { v$.value.$reset(); + copilot.reset(false); removeSignatureFromMessage(); stripMessageFormatting(DEFAULT_FORMATTING); @@ -231,6 +244,7 @@ const removeTargetInbox = value => { }; const clearSelectedContact = () => { + copilot.reset(false); removeSignatureFromMessage(); emit('clearSelectedContact'); state.message = ''; @@ -262,6 +276,7 @@ const handleAttachFile = files => { }; const clearForm = () => { + copilot.reset(false); Object.assign(state, { message: '', subject: '', @@ -324,6 +339,24 @@ const shouldShowMessageEditor = computed(() => { !inboxTypes.value.isTwilioWhatsapp ); }); + +const isCopilotActive = computed(() => copilot.isActive?.value ?? false); + +const onSubmitCopilotReply = () => { + const acceptedMessage = copilot.accept(); + state.message = acceptedMessage; +}; + +useKeyboardEvents({ + '$mod+Enter': { + action: () => { + if (isCopilotActive.value && !copilot.isButtonDisabled.value) { + onSubmitCopilotReply(); + } + }, + allowOnFocusedInput: true, + }, +});