Commit Graph
4724 Commits
Author SHA1 Message Date
Pranav 8993810af8 Merge remote-tracking branch 'origin/develop' into captain-message-generation-metadata
# Conflicts:
#	config/routes.rb
#	db/schema.rb
#	enterprise/app/models/enterprise/concerns/message.rb
2026-07-02 07:09:59 -07:00
Sivin VargheseandGitHub 90b9dba9f9 fix: correct report drilldown drawer position in RTL/LTR (#14919) 2026-07-02 18:44:25 +05:30
Sivin VargheseandGitHub 2a21d075f6 fix: preserve link URLs on channels without hyperlink support (#14912) 2026-07-02 17:23:53 +05:30
6a7ca9dd3b feat: Add report bar drilldown drawer (#14626)
## Description

Adds drilldown support for report bar charts powered by
`ReportContainer`. Clicking a non-zero report bar now opens a right-side
drawer with the conversations or messages that contributed to that
bucket, with each row linking to the underlying conversation and message
rows linking with `messageId`.

This includes a new `GET /api/v2/accounts/:account_id/reports/drilldown`
endpoint, backend drilldown builders/serializers, generic chart click
emission, local drawer state via `useReportDrilldown`, compact drilldown
cards, pagination, stale-response protection, and validation for
unsupported drilldown dimensions.

Fixes # CW-4497

https://linear.app/chatwoot/issue/CW-4497/drill-down-on-agent-conversations-report

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Ran the focused backend and frontend checks for the drilldown endpoint,
builder, chart click handling, drawer/card UI, API helper, and
stale-response handling.

Here are the screenshots on how it looks like:
<img width="1792" height="1199" alt="Screenshot 2026-06-02 at 11 32
11 PM"
src="https://github.com/user-attachments/assets/6bdb8832-b9df-4bf3-9a2a-beaefe203b6e"
/>
<img width="1791" height="1230" alt="Screenshot 2026-06-02 at 11 32
34 PM"
src="https://github.com/user-attachments/assets/36e92eb7-3208-4855-87f4-0c7f316df54d"
/>
<img width="1784" height="1235" alt="Screenshot 2026-06-02 at 11 32
46 PM"
src="https://github.com/user-attachments/assets/f7a53916-74f2-4622-9305-042e0ac9e877"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-07-02 16:07:26 +05:30
Sivin VargheseandGitHub 34d8741b9b feat: drag to reorder help center articles across pages (#14910) 2026-07-02 15:47:01 +05:30
7bf76057c2 fix: SLA handling for blocked contacts (#14861)
# Pull Request Template

## Description

Blocked contacts are now excluded from SLA assignment, processing,
reports, and conversation SLA UI while they remain blocked. Existing SLA
records are preserved, and SLA behavior resumes if the contact is
unblocked.

Fixes
https://linear.app/chatwoot/issue/CW-7435/sla-should-not-trigger-for-blocked-contacts

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- `bundle exec rspec spec/enterprise/models/conversation_spec.rb
spec/enterprise/models/applied_sla_spec.rb
spec/enterprise/services/enterprise/action_service_spec.rb
spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb
spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb
spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/presenters/conversations/event_data_presenter_spec.rb` —
78 examples, 0 failures
- `bundle exec rubocop
enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
enterprise/app/jobs/sla/process_account_applied_slas_job.rb
enterprise/app/models/applied_sla.rb
enterprise/app/models/enterprise/concerns/conversation.rb
enterprise/app/presenters/enterprise/conversations/event_data_presenter.rb
enterprise/app/services/enterprise/action_service.rb
enterprise/app/services/sla/evaluate_applied_sla_service.rb
lib/tasks/apply_sla.rake
spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb
spec/enterprise/models/applied_sla_spec.rb
spec/enterprise/models/conversation_spec.rb
spec/enterprise/presenters/conversations/event_data_presenter_spec.rb
spec/enterprise/services/enterprise/action_service_spec.rb
spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb` — no
offenses
- `pnpm exec vitest --no-watch --no-cache --no-coverage
app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js`
— 2 tests passed
- `pnpm exec eslint
app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCardExpanded.vue
app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js`
— passed with existing raw-text warnings in `ConversationHeader.vue`
- `git diff --cached --check` — clean

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-02 14:16:29 +05:30
Tanmay Deep SharmaandGitHub a3c7f3b204 fix: finalize WhatsApp calls when terminate webhook overtakes connect (#14836)
## Description

Some inbound WhatsApp calls stayed stuck in "ringing" forever. When a
caller hung up within ~1s of dialing, Meta delivered the terminate
webhook before the connect webhook. The terminate arrived with no call
record yet and was dropped, then connect created the call in ringing
with nothing left to close it. These calls now correctly land as missed
(no_answer), and an agent who taps Accept on a call that already ended
gets a clean "call ended" instead of a generic error.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- local UI testing

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-07-02 13:58:36 +05:30
49b0ab0e1f fix: Consider business hours when computing SLA breaches (#13392)
- Fixes SLA breach computation to respect the "Only during business
hours" setting
- Backend now pre-computes SLA deadlines, simplifying frontend logic

## How it works

Before: SLA deadlines were calculated using wall-clock time, ignoring
business hours.

After: When an SLA policy has "Only during business hours" enabled and
the inbox has working hours configured, the deadline is calculated by
adding threshold time only during business hours.


**How you check if a conversation has a SLA hit or miss?**

<img width="474" height="510" alt="Screenshot 2026-01-28 at 7 06 53 PM"
src="https://github.com/user-attachments/assets/54ec8581-18b8-45c6-a356-de8c778ea78d"
/>


**Example:**
- Conversation created: Friday 4:30 PM
- FRT threshold: 1 hour
- Business hours: Mon-Fri 9 AM - 5 PM

| | Breach time |
|--|--|
| Before | Friday 5:30 PM |
| After | Monday 9:30 AM |

## Test plan

- [x] Create an SLA policy with "Only during business hours" enabled
- [x] Configure inbox with business hours (e.g., Mon-Fri 9-5)
- [x] Conversation created during business hours
  - Create a conversation on Wednesday 10:00 AM UTC
- Expected: FRT deadline shows Wednesday 12:00 PM UTC (2 business hours
later)
- [x] Conversation created before business hours
  - Create a conversation on Wednesday 7:00 AM UTC
- Expected: FRT deadline shows Wednesday 11:00 AM UTC (counting starts
at 9 AM)
- [x] Conversation created after business hours
  - Create a conversation on Wednesday 6:00 PM UTC
- Expected: FRT deadline shows Thursday 11:00 AM UTC (counting starts
next day 9 AM)
- [x] Conversation created on weekend
  - Create a conversation on Saturday 10:00 AM UTC
  - Expected: FRT deadline shows Monday 11:00 AM UTC (skips weekend)
- [x] Threshold spans weekend
  - Create a conversation on Friday 4:00 PM UTC with 2-hour FRT
- Expected: FRT deadline shows Monday 10:00 AM UTC (1h Friday + 1h
Monday)
- [x] SLA without business hours
  - Create an SLA policy with only_during_business_hours: false
  - Create a conversation on Friday 4:00 PM UTC with 2-hour FRT
  - Expected: FRT deadline shows Friday 6:00 PM UTC (wall-clock time)
- [x] All Day marked as closed_all_day
  - Create a conversation on Tuesday 4:00 PM UTC with 2-hour FRT
  - Expected: FRT deadline shows Thursday 10:00 AM UTC
- [x] All Day marked as open_all_day
  - Create a conversation on Saturday 10:00 AM UTC with 2-hour FRT
  - Expected: FRT deadline shows Saturday 12:00 PM UTC 
- [x] UI displays correct countdown
  - Verify conversation card shows correct SLA timer
  - Verify timer shows flame icon when breached
  - Verify timer shows alarm icon when within threshold
  - Time updates automatically when time passes
- [x] Verify the breach with a different timezone than your local
timezone

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-07-02 13:33:22 +05:30
Aakash BakhleandGitHub 926a9d8a69 fix(captain): default temperature to 0.5 and remove UI control (#14879)
# Pull Request Template

## Description

- Default temperature to 0.5 and remove UI control
- No migrations needed for existing accounts, their current settings are
preserved

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

locally and spec

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-06-30 15:14:03 +05:30
Sivin VargheseandGitHub 2767bd434b fix: Show a clear error when message translation fails (#14891) 2026-06-30 14:23:47 +05:30
299bc6c0a4 fix: recover from stale LeadSquared lead ids on activity sync (#14818)
LeadSquared sync now recovers automatically when a contact's cached lead
has been deleted or merged on the LeadSquared side. Previously the stale
lead id was never cleared, so every new conversation or contact update
for that contact failed with "Lead not found"
(`MXInvalidEntityReferenceException`) indefinitely.

## What changed
- Activity sync: on a "Lead not found" error while posting a
conversation/transcript activity, clear the cached `leadsquared_id`,
re-resolve the contact to a fresh lead, and retry the activity once
(guarded against loops and duplicate leads).
- Contact sync: on the same error while updating an existing lead, clear
the cached id and create a fresh lead instead.
- Fix `get_lead_id` to actually return early for unidentifiable contacts
(the guard previously fell through).

## How to reproduce
1. For a LeadSquared-enabled account, point a contact's cached lead id
at a lead that no longer exists in LeadSquared.
2. Update the contact, or create/resolve a conversation for it.
3. Before: the sync fails repeatedly with "Lead not found" and never
self-corrects. After: the stale id is cleared, a fresh lead is
resolved/created, and subsequent syncs reuse the healed id.

---------

Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
2026-06-29 16:38:08 +05:30
Sony MathewandGitHub cf134deb37 fix: Preserve Captain LLM defaults (#14858)
# Pull Request Template

## Description

Adjusts the Captain LLM feature defaults after feature routing so the
defaults stay intentional and avoid unintended high-cost model upgrades.
Assistant, copilot, and onboarding content generation now default to
`gpt-4.1`; audio transcription keeps `gpt-4o-mini-transcribe` as the
default while exposing `whisper-1` as an available account override
option.

Related: https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- `ruby -ryaml -e 'yaml = YAML.load_file("config/llm.yml"); features =
yaml.fetch("features"); models = yaml.fetch("models"); features.each {
|name, cfg| missing = Array(cfg["models"]) - models.keys; raise
"#{name}: missing #{missing.join(",")}" if missing.any?; raise "#{name}:
default not in models" unless
Array(cfg["models"]).include?(cfg["default"]) }'`
- `node -e
"JSON.parse(require('fs').readFileSync('app/javascript/dashboard/i18n/locale/en/settings.json',
'utf8'))"`
- `pnpm exec prettier --check
app/javascript/dashboard/i18n/locale/en/settings.json`
- `bundle exec rspec spec/lib/llm/feature_router_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/models/concerns/agentable_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/models/account_spec.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/controllers/super_admin/accounts_controller_spec.rb`
- `bundle exec rspec
spec/enterprise/services/captain/llm/article_translation_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb`
- `RUBOCOP_CACHE_ROOT=/private/tmp/rubocop_cache bundle exec rubocop
enterprise/app/services/captain/llm/article_translation_service.rb
enterprise/app/services/messages/audio_transcription_service.rb
spec/enterprise/services/captain/llm/article_translation_service_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 21:35:09 +05:30
Sony MathewandGitHub b8b62ad0f1 feat: Harden model override preferences (5/6) (#14846)
## Description

Hardens the Captain model override preferences API so account-level
overrides follow the same feature-router contract used by runtime LLM
calls. The API now permits model and feature keys from `llm.yml`,
removes blank model overrides, rejects invalid saved model combinations,
and returns each feature's effective model, provider, and source for UI
clients.

Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Verified the account preferences API and account model validation
behavior for valid overrides, invalid model values, unknown feature
keys, blank override removal, and effective model/provider/source
payload metadata.

- `eval "$(rbenv init -)" && bundle exec rspec
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/models/account_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/lib/llm/feature_router_spec.rb`
- `eval "$(rbenv init -)" && bundle exec rubocop
app/controllers/api/v1/accounts/captain/preferences_controller.rb
app/models/concerns/account_settings_schema.rb
app/models/concerns/captain_featurable.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/models/account_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/lib/llm/feature_router_spec.rb`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 17:38:11 +05:30
Sony MathewandGitHub 43e0f8a141 feat: Route Enterprise LLM services (3/6) (#14841)
# Pull Request Template

## Description

Routes Enterprise assistant, copilot, FAQ, contact memory,
action-classifier, and false-promise detector LLM paths through
feature-specific model resolution. `Llm::BaseAiService` now accepts
feature/account context and uses `Llm::FeatureRouter` when that context
is present, while retaining the installation-model fallback for
unmigrated callers. This also adds a `document_faq_generation` feature
default for generative FAQ/document content.

Linear: https://linear.app/chatwoot/issue/CW-7425/test-new-models
Depends on #14840

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

- `bundle exec rspec spec/lib/llm/models_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/captain/copilot/chat_service_spec.rb
spec/enterprise/services/captain/llm/assistant_chat_service_spec.rb
spec/enterprise/services/captain/llm/faq_generator_service_spec.rb
spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb
spec/enterprise/services/captain/llm/assistant_action_classifier_service_spec.rb
spec/enterprise/services/captain/llm/assistant_false_promise_service_spec.rb
spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb`
passed with 112 examples, 0 failures.
- `bundle exec rspec spec/models/concerns/captain_featurable_spec.rb
spec/models/account_spec.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/lib/llm/feature_router_spec.rb` passed with 87 examples, 0
failures.
- `bundle exec rubocop enterprise/app/services/llm/base_ai_service.rb
enterprise/app/services/captain/copilot/chat_service.rb
enterprise/app/services/captain/llm/assistant_chat_service.rb
enterprise/app/services/captain/llm/faq_generator_service.rb
enterprise/app/services/captain/llm/conversation_faq_service.rb
enterprise/app/services/captain/llm/contact_notes_service.rb
enterprise/app/services/captain/llm/contact_attributes_service.rb
enterprise/app/services/captain/llm/assistant_action_classifier_service.rb
enterprise/app/services/captain/llm/assistant_false_promise_service.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/captain/copilot/chat_service_spec.rb
spec/enterprise/services/captain/llm/assistant_chat_service_spec.rb
spec/enterprise/services/captain/llm/faq_generator_service_spec.rb
spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb
spec/enterprise/services/captain/llm/assistant_action_classifier_service_spec.rb
spec/enterprise/services/captain/llm/assistant_false_promise_service_spec.rb
spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb`
passed with no offenses.
- `bundle exec ruby -e "require 'yaml'; config =
YAML.load_file('config/llm.yml'); abort('missing
document_faq_generation') unless config.dig('features',
'document_faq_generation'); abort('missing default') unless
config.dig('features', 'document_faq_generation', 'default'); puts
'llm.yml ok'"` passed.
- `git diff --check` passed.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 17:03:07 +05:30
Sony MathewandGitHub 8b977b35a8 feat: Add LLM feature router (1/6) (#14839)
## Description

Adds the foundation for feature-specific LLM model routing so Captain AI
features can resolve their effective provider/model from code defaults
and account-level overrides. This fixes the provider metadata key in
`config/llm.yml`, adds `Llm::FeatureRouter`, and routes existing
`CaptainFeaturable` model defaults through the shared resolver.

Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

- `bundle exec rspec spec/lib/llm/models_spec.rb
spec/lib/llm/feature_router_spec.rb
spec/models/concerns/captain_featurable_spec.rb` - 23 examples, 0
failures
- `bundle exec rubocop lib/llm/models.rb lib/llm/feature_router.rb
app/models/concerns/captain_featurable.rb spec/lib/llm/models_spec.rb
spec/lib/llm/feature_router_spec.rb
spec/models/concerns/captain_featurable_spec.rb` - no offenses
- `bundle exec ruby -e "require 'yaml'; config =
YAML.load_file('config/llm.yml'); abort('missing providers') unless
config['providers']; abort('missing models') unless config['models'];
abort('missing features') unless config['features']; puts 'llm.yml ok'"`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 17:00:07 +05:30
62cbeae95f feat: onboarding inboxes UI (#14565)
After entering their account details, new admins land on an **Inbox
setup** screen that shows what we've already set up for them and lets
them connect their conversation channels without leaving onboarding. It
surfaces the auto-created live chat widget (and Help Center on
Enterprise), highlights channels detected from their website, and offers
a **View all** dialog to connect any supported channel inline.

### Channel status

| Channel | How it connects | Status | PR |
|---|---|---|---|
| Live chat (Website) | Auto-created during setup |  Done |
https://github.com/chatwoot/chatwoot/pull/14314 |
| WhatsApp | Meta embedded signup |  Done |
https://github.com/chatwoot/chatwoot/pull/14619 |
| Facebook | Login + page picker |  Done |
https://github.com/chatwoot/chatwoot/pull/14619 |
| Instagram | OAuth redirect |  Done |
https://github.com/chatwoot/chatwoot/pull/14568 |
| TikTok | OAuth redirect |  Done |
https://github.com/chatwoot/chatwoot/pull/14569 |
| LINE | Inline credential form |  Done | — |
| Telegram | Inline credential form |  Done | — |
| Gmail / Outlook | OAuth (email) | ⚠️ Disabled — coming in a follow-up
| https://github.com/chatwoot/chatwoot/pull/14567 |
| SMS / API / Voice / Other email | — |  Unavailable | — |

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-25 14:54:07 +05:30
7c7459b734 fix(whatsapp): override webhook at phone number level (#13817)
## Description

Move WhatsApp webhook callback override from WABA level to phone number
level, allowing multiple phone numbers on the same WABA to have
independent callback URLs.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Connect a WhatsApp Cloud inbox via embedded signup
- Verify webhook setup succeeds and messages are received
- Connect a second phone number on the same WABA — both should receive
messages independently
- Delete an inbox and verify only that phone number's override is
cleared

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes

---------

Co-authored-by: tds-1 <tds-1@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-25 14:49:09 +05:30
f9cc702030 fix(help-center): documentation layout on custom domain root, locale (#14850)
Help centers using the documentation layout now render correctly in two
cases that previously fell back to the wrong output. Opening a portal at
its custom-domain root (e.g. docs.example.com/ ) now shows the full
documentation home
in place instead of the classic layout, and portals whose locale is a
region variant that Chatwoot doesn't ship a
translation for (e.g. th_TH , fr_ML ) now show their categories in the
sidebar on article pages.

Closes
https://linear.app/chatwoot/issue/CW-7437/portal-layout-is-not-properly-working-in-custom-domain

## How to test

1. Create a portal with the documentation layout and a custom domain
(e.g. example.chat.test ), with at least one
category containing a published article.
2. Visit the custom-domain root ( http://example.chat.test:3000/ ) → it
should show the full documentation home
(sidebar, topbar), not the classic layout, with no redirect.
3. Set the portal's locale to a region variant Chatwoot doesn't
translate, e.g. th_TH , with categories/articles
under that locale.
4. Open an article → the sidebar should list the article's category and
sibling articles. Before the fix the sidebar
was empty for these locales.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:12:55 -07:00
74db16158d feat: migrate dyte integration to cloudflare realtimekit (#14752)
Dyte is sunsetting its existing infrastructure after the Cloudflare
acquisition, so this migrates Chatwoot’s video call integration to
Cloudflare RealtimeKit.

The integration now uses Cloudflare Account ID, RealtimeKit App ID, and
a Cloudflare API token with Realtime Admin permissions. Meeting creation
and participant token generation now call Cloudflare’s RealtimeKit APIs,
while the existing Chatwoot call experience remains unchanged for agents
and customers.

This also adds setup-time credential validation, so admins get clearer
errors when the API token is invalid, the Cloudflare account or
permissions are incorrect, or the RealtimeKit App ID does not belong to
the selected account.

Fixes
https://linear.app/chatwoot/issue/PLA-176/migrate-dyte-integration-to-cloudflare-realtimekit

**How to test**

1. Go to Settings → Integrations → Cloudflare RealtimeKit.
2. Add a Cloudflare Account ID, RealtimeKit App ID, and API token with
Realtime Admin permissions.
3. Confirm the integration saves successfully with valid credentials.
4. Try invalid credentials and confirm the error identifies whether the
token, account/permissions, or app ID is wrong.
5. Start a video call from a conversation and confirm the RealtimeKit
meeting opens.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-24 11:04:08 +04:00
Sivin VargheseandGitHub e8edc9ebf5 feat: allow users to set an icon or emoji for teams (#14767) 2026-06-23 20:53:31 +05:30
Vishnu NarayananandGitHub e785620921 feat: read structured X-Chatwoot-* headers for mobile session metadata (#14762)
Phase 2 of [INF-75](https://linear.app/chatwoot/issue/INF-75). Follows
up on [#14753](https://github.com/chatwoot/chatwoot/pull/14753) (Phase 1
UA pattern fallback, already merged).

When the request carries an \`X-Chatwoot-Client-Name\` header,
\`UserSessionTrackingService\` now prefers the five structured
\`X-Chatwoot-*\` headers over the User-Agent for populating the
\`user_sessions\` row:

| Header | Mapped column |
|---|---|
| \`X-Chatwoot-Client-Name\` | \`browser_name\` |
| \`X-Chatwoot-Client-Version\` | \`browser_version\` |
| \`X-Chatwoot-Device-Model\` | \`platform_name\` |
| \`X-Chatwoot-Platform-Version\` | \`platform_version\` |
| \`X-Chatwoot-Platform\` (+ model) | \`device_name\` (\`iPhone\` /
\`iPad\` / \`Android\`) |

When the header is absent or blank, the existing \`Browser.new\` path
runs, with the Phase 1 legacy UA fallback still acting as the floor.
Real browsers are untouched.

A follow-up PR in
[chatwoot-mobile-app](https://github.com/chatwoot/chatwoot-mobile-app)
will start sending these headers from the React Native build. Until that
ships, this PR is a no-op for production traffic, so it can land
independently.

Fixes INF-75.
2026-06-23 20:08:27 +05:30
Aakash BakhleandGitHub 84fda8a323 fix(captain): move false promise harness setting to account (#14823)
# Pull Request Template

## Description

moves the harness to account settings rather than assistant settings

## Type of change
refactor

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
locally and specs

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-06-23 16:55:42 +05:30
ee5e20551d fix(facebook): handle messenger post attachment type (#14813)
Facebook Messenger messages that share a post no longer break message
ingestion. Following the recent sticker change, Meta now sends a shared
post as a new `post` attachment type. Chatwoot didn't recognise `post`
as a valid attachment file type, so the webhook job crashed and the
message — and any others in the same batch — failed to sync. Shared
posts now appear in the conversation as a fallback link (title + URL),
the same way `share` attachments already do.

This is the same class of bug as #14793 (sticker), just a different
attachment type.

#### Root cause

`post` was neither skipped as unsupported nor mapped to a valid
`file_type`, so `attachment_params` produced `file_type: :post`. The
`Attachment#file_type` enum has no `post` value, so it raised
`ArgumentError: 'post' is not a valid file_type`, crashing
`Webhooks::FacebookEventsJob`.

#### Fix

The Facebook builder already maps `share -> :fallback` because shared
posts point to facebook.com page URLs rather than downloadable media. A
`post` is the same kind of attachment, so map both `share` and `post` to
`fallback` (stores the title/link, no download attempt). Also read the
fallback title from `payload.title`, since the post payload nests it
there rather than at the top level.

## How to reproduce

1. Connect a Facebook Page inbox.
2. Share a Facebook post into Messenger to that page.
3. Before this change: `Webhooks::FacebookEventsJob` raises
`ArgumentError: 'post' is not a valid file_type` and the message is
dropped.
4. After this change: the shared post shows up in the conversation as a
fallback link.

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:04 +04:00
df79c0bbde feat: exclude stale conversations via assignment policy age threshold (#14766)
## Linear ticket
-
https://linear.app/chatwoot/issue/CW-7137/assignment-v2-backlog-flush-overloads-agents

## Description

Assignment policies now skip stale unassigned conversations
automatically. Each policy carries an age threshold (defaults to 7
days), so auto-assignment only picks up recent backlog instead of
draining very old, forgotten conversations. The threshold is
configurable per policy and can be cleared to assign conversations
regardless of age. Previously this control existed only on Enterprise
capacity policies (`exclude_older_than_hours`); it now lives on the
assignment policy itself, so every V2 inbox benefits without needing a
capacity policy.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Local UI flows

## Screenshots? 

<img width="645" height="822" alt="image"
src="https://github.com/user-attachments/assets/ec58db86-c1fa-4f9e-be87-2e24ff02e077"
/>


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-23 12:22:45 +05:30
647cfc2d83 feat: distinguish agent-declined calls with a rejected status (#14783)
## Linear ticket
-
https://linear.app/chatwoot/issue/CW-7374/agent-declined-voice-calls-miscounted-as-failed

## Description

Agent rejections were stored with status: failed, so call reports lumped
deliberate declines together with real technical failure.

Fix: give declines their own terminal rejected status (Twilio + WhatsApp
paths), keeping end_reason: agent_rejected. Genuine provider/network
failures stay failed. Frontend renders declines exactly as before;
existing rows backfilled via migration.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Tested on UI

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-23 12:22:29 +05:30
Sivin VargheseandGitHub 961ec86ba9 chore: support custom variables in auto-resolution message (#14782)
# Pull Request Template

## Description

This PR adds support for custom variables in auto-resolution messages
within Conversation Workflows.

Agents can insert variables (e.g. `{{contact.name}}`) into the custom
auto-resolution message using the variable picker by typing `{{`. These
variables are resolved with actual conversation data when the
auto-resolution message is sent.

The field uses the existing editor with the formatting toolbar hidden,
since auto-resolution messages are delivered across all channels,
including SMS, where formatting isn't supported. Existing message
content is preserved as-is, and typing `{{` opens the variable picker
for quick insertion.

Fixes
https://linear.app/chatwoot/issue/CW-7369/support-custom-variables-in-conversations-workflow-text-fields

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshots
<img width="1262" height="764" alt="image"
src="https://github.com/user-attachments/assets/e43be22c-f86e-436a-9fdd-5bcb73c61ca3"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-23 11:35:52 +05:30
a4be38f8f3 fix(facebook): handle messenger sticker attachment type (#14793)
Facebook Messenger sticker messages no longer break message ingestion.
Meta recently changed its webhook payloads so a sticker now arrives as a
new `sticker` attachment type (alongside the existing `image` attachment
during the transition period). Chatwoot didn't recognise `sticker` as a
valid attachment file type, so the webhook job crashed and the message —
and any others in the same batch — failed to sync. Stickers now appear
in the conversation as a single image, just like before.

Fixes https://linear.app/chatwoot/issue/PLA-177

## How to reproduce

1. Connect a Facebook Page inbox.
2. Send a sticker from Messenger to that page.
3. Before this change: the `Webhooks::FacebookEventsJob` raises
`ArgumentError: 'sticker' is not a valid file_type` and the message is
dropped.
4. After this change: the sticker shows up in the conversation as a
single image attachment.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:40:23 +04:00
PranavandClaude Opus 4.8 eb203bb144 feat(captain): capture and surface how a reply was generated
Records reasoning, citations (the FAQs the assistant found), the tool
path, and the model for each V1 Captain reply in a new
captain_message_generations table. The assistant reports the source IDs
it actually used so cited FAQs can be flagged, and handoff messages
capture the transfer reason.

Agents can expand any Captain message in the conversation (a sparkle
toggle on the timestamp line) to see this breakdown, fetched on demand
from a dedicated endpoint. Model is shown only in development.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:10:55 -07:00
6a262287d2 feat(captain): allow agents to report Captain messages (#14799)
Adds a cloud-only flow for agents to flag incorrect or problematic
Captain (AI) responses. Right-clicking a Captain message surfaces a
"Report message" option that opens a dialog to pick a problem type and
add a description, persisted to a new captain_message_reports table for
the team to review.


<img width="636" height="542" alt="Screenshot 2026-06-20 at 9 15 56 AM"
src="https://github.com/user-attachments/assets/afaa233d-6bd6-455a-8a33-a3796a3e3ef6"
/>
<img width="580" height="502" alt="Screenshot 2026-06-20 at 9 16 03 AM"
src="https://github.com/user-attachments/assets/2d220d99-98cc-4c5e-a325-778ceb4f7bc9"
/>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:47:08 -07:00
80b132e155 feat(tiktok): add cloud warning (#14768)
Adds Cloud-only warning on the TikTok connect page while North American
account connections may be temporarily unavailable.

Fixes
https://linear.app/chatwoot/issue/CW-7322/cloud-show-tiktok-north-america-warning-on-connect-page

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-19 12:29:07 +04:00
Sivin VargheseandGitHub 32b63ad0c0 feat: add search to Help Center admin pages (#14772) 2026-06-19 12:41:21 +05:30
Sivin VargheseandGitHub ac15339456 fix: Resolve Firefox input issues and persist advanced filters (#14781)
# Pull Request Template

## Description

This PR fixes a few issues in Global Search and removes a non-functional
control.

* Fixes an issue in Firefox where characters could be dropped while
typing in the search input. The search now uses the latest input value
directly, preventing searches from running one character behind.

* Removes a stale query sync in `SearchHeader` that could overwrite
recently typed characters during the debounce window, causing the input
to appear out of sync.

* Fixes advanced search filters being removed from the URL on page
reload. The search page now waits for account data to load before
parsing URL parameters, ensuring agent, inbox, and date range filters
are preserved.

* Removes the non-functional "Sort by relevance" button from the search
tabs bar, as it was disabled and had no effect.


Fixes https://github.com/chatwoot/chatwoot/issues/14684
[CW-7305](https://linear.app/chatwoot/issue/CW-7305/global-search-drops-characters-while-typing-in-firefox-query-truncated)
[CW-7370](https://linear.app/chatwoot/issue/CW-7370/remove-non-functional-relevance-placeholder-button-from-global-search)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Screencast

**Before**


https://github.com/user-attachments/assets/48d72a4e-20f4-4f24-91f4-2c9a9c065eba




**After**


https://github.com/user-attachments/assets/0e41a803-b4fd-410d-a739-549f72d418d6





## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-19 10:46:47 +04:00
Vishnu NarayananandGitHub f66b551c7d revert: Sidebar unread counts for filters (CW-7262) (#14769)
## Description

Reverts [#14726](https://github.com/chatwoot/chatwoot/pull/14726)
(\"feat: Add sidebar unread counts for filters (CW-7262)\"), which
shipped in 4.15.0.

After 4.15.0 rolled out to prod the unread-counts-for-filters code path
caused a cascading incident:

- `Counter#ensure_filters_cache!` fires on every `/unread_counts/index`
and `update_last_seen` request.
- On cache miss it calls `Builder#build_filters_for!`, which:
- invokes `store.clear_user_filters!` -> `delete_matching` -> a Redis
`SCAN_each` over a per-user pattern keyspace, and
- runs 4 fresh SQL passes per user (mentions, participating, unattended,
and per-folder `Conversations::FilterService` queries).
- Threads blocked in the SCAN held their DB connections, the connection
pool exhausted, Sidekiq jobs were discarded with
`ActiveJob::DeserializationError: could not obtain a connection from the
pool`, and the enqueued queue blew past 200K.


Related:
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-17 14:58:48 +04:00
Vishnu NarayananandGitHub c70a57407d fix: label Chatwoot Mobile sessions instead of 'Unknown Device' (#14753)
## Description

Sessions created by logins from the Chatwoot Mobile app currently render
as "Unknown Device" in the dashboard sessions UI. The mobile app's HTTP
layer sends:

- Android: `User-Agent: okhttp/4.9.2`
- iOS: `User-Agent: Chatwoot/<build> CFNetwork/<v> Darwin/<v>`

Neither pattern is classifiable by the `browser` gem, so
\`browser_name\`, \`platform_name\`, and \`device_name\` all end up
"Unknown".

This change adds a backend-only fallback in
\`UserSessionTrackingService\`. When \`Browser.new(ua)\` returns
"Unknown Browser" and the UA matches a known Chatwoot Mobile pattern,
the labels are overridden to \`Chatwoot Mobile\` + \`Android\` /
\`iPhone\`. The Vue sessions list (\`ActiveSessions.vue\`) then renders
"Chatwoot Mobile on Android" or "Chatwoot Mobile on iPhone" with the
smartphone icon.

This is the immediate floor. A follow-up will add structured
\`X-Chatwoot-*\` headers from the mobile app so we can render full
version + device model.

Fixes INF-75.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Added specs

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-06-17 00:05:29 +05:30
fe6368b42e feat: Add sidebar unread counts for filters (CW-7262) (#14726)
## Description

Extends the conversation unread-count system so the left sidebar can
show unread badges for Mentions, Participating, Unattended, and saved
conversation folders. Folder badges reuse the existing `custom_filters`
conversation filter semantics, store user-scoped Redis sets lazily, and
skip unsupported folder filters so invalid saved folders continue to
render without a badge. The Unattended badge counts all visible unread
open conversations that match the existing unattended conversation
scope.

Closes
-
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## What changed

- Added user-scoped unread-count Redis keys and cache builders for
mentions, participating conversations, unattended conversations, and
saved folder filters.
- Reused `Conversations::FilterService` through a relation-returning
path so folder counts match the folder conversation list behavior.
- Invalidated user filter caches from mention, participant,
custom-filter, and relevant conversation update events.
- Extended the unread-count endpoint payload and sidebar Vuex/sidebar
rendering for the new badge counts, including the Unattended sidebar
item.
- Added Ruby, Enterprise, request, listener, and frontend store coverage
for the new unread-count dimensions.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Created local validation folders for `john@acme.inc` and confirmed the
unread-count payload includes open, resolved, and high-priority folder
badges while excluding the invalid unsupported folder.
- Added coverage for the Unattended badge rule: all visible unread open
conversations matching `Conversation.unattended`.
- Ran focused unread-count Ruby specs, including service, listener,
request, and Enterprise counter coverage.
- Ran frontend unread-count store specs.
- Ran RuboCop on the touched Ruby files.
- Ran ESLint through the project script; it completed with warnings in
existing unrelated files and no errors.

<img width="369" height="525" alt="Screenshot 2026-06-13 at 10 51 39 PM"
src="https://github.com/user-attachments/assets/36b1d2c4-dac1-4f6f-9c0e-7ef5a6cc2975"
/>

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] Documentation changes are not required for this internal
unread-count behavior
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] No dependent downstream changes are required

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-16 23:46:49 +05:30
Sivin VargheseandGitHub 352f120c6a feat: add dedicated color tokens for voice call widget (#14745)
# Pull Request Template

## Description

This PR introduces dedicated color tokens for the floating voice call
widget instead of reusing generic design tokens. This allows the widget
to maintain a distinct dark-card appearance across both light and dark
themes.

Adds the following tokens and applies them to `CallCard`:

* `--call-widget`
* `--call-widget-border`
* `--call-widget-text`
* `--call-widget-sub-text`

Fixes
https://linear.app/chatwoot/issue/CW-7353/call-notification-window-theme-update-design

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshots

**Light mode**
<img width="1466" height="815" alt="Screenshot 2026-06-16 at 3 47 32 PM"
src="https://github.com/user-attachments/assets/732164b1-5488-4cc1-8c71-01245d906842"
/>


**Dark mode**
<img width="1466" height="815" alt="Screenshot 2026-06-16 at 3 46 54 PM"
src="https://github.com/user-attachments/assets/536ab632-4c7c-486b-8fc4-a061ee40b22a"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-16 20:01:31 +05:30
41a3ab6dfa feat(companies): add contact company selector (#14496)
Adds a company selector to the contact details form so agents can
associate a contact with an existing company directly from the contact
page.

Closes

- None

Why

Contacts already expose company information through the CRM fields, but
the form only accepted free-text company names. As we split company CRM
work into smaller PRs, this keeps the contact page aligned with the
structured company model while preserving the existing company-name
behavior used by automations.

What changed

- Shows a company dropdown in the contact details form when the
Companies feature is enabled.
- Keeps legacy free-text company names editable when a contact has no
structured `company_id`.
- Allows Enterprise contact create/update APIs to accept account-scoped
`company_id`.
- Syncs `additional_attributes.company_name` when a contact is
associated with a company, including the existing email-domain
auto-association path.
- Serializes `company_id` in the contact model payload so the form can
show the current association.

How to test

1. Enable Companies for an account and open a contact details page.
2. In Edit contact details, use the Company field to select an existing
company.
3. Save the contact and refresh the page.
4. Confirm the selected company remains visible and the contact is
associated with that company.
5. Confirm contacts with only a legacy free-text company name still show
the text input instead of an empty selector.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-16 15:29:27 +05:30
de137e8297 feat: Capture CTWA referral metadata for WhatsApp conversations (#14681)
Adds support for capturing Click-to-WhatsApp ad referral metadata from
incoming WhatsApp messages.

This stores Meta Cloud API `referral` payloads on the incoming message
`content_attributes` and normalizes Twilio `Referral*` callback fields
into the same shape. The UI display is intentionally deferred until
Instagram, Messenger, and TikTok referral payloads are captured as well,
so we can design one cross-channel referral surface instead of a
WhatsApp-only sidebar block.

Why message-level storage

Meta sends CTWA referral details as part of the inbound message payload,
not as a stable conversation-level webhook. The referral represents the
exact ad click that produced that specific customer message, and later
messages in the same conversation may not carry the same context.
Storing the normalized referral on the message preserves the original
webhook semantics, avoids adding conversation-level attribution that
could become stale or ambiguous, and keeps support for both Cloud API
and Twilio payloads aligned behind `content_attributes.referral`.
Fixes
https://linear.app/chatwoot/issue/CW-7090/surface-meta-ctwa-referral-on-incoming-whatsapp-messages-cloud-api
Closes https://github.com/chatwoot/chatwoot/issues/13995,
https://github.com/chatwoot/chatwoot/issues/12560,
https://github.com/chatwoot/chatwoot/issues/13006


Related community PRs
- https://github.com/chatwoot/chatwoot/pull/13130
- https://github.com/chatwoot/chatwoot/pull/14180
- https://github.com/chatwoot/chatwoot/pull/14121

Related follow-ups
-
https://linear.app/chatwoot/issue/CW-7301/capture-instagram-ad-referral-metadata-on-incoming-messages
-
https://linear.app/chatwoot/issue/CW-7302/capture-messenger-ad-referral-metadata-on-facebook-page-conversations
-
https://linear.app/chatwoot/issue/CW-7303/capture-tiktok-ad-referral-metadata-from-im-referral-msg-events

How to test
1. Send or replay a WhatsApp Cloud API inbound message that contains a
`messages[0].referral` payload from a Click-to-WhatsApp ad.
2. Confirm the generated incoming message stores the payload under
`content_attributes.referral`.
3. Repeat with a Twilio WhatsApp callback containing `Referral*` fields
and confirm the stored message has the same normalized
`content_attributes.referral` structure.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-16 09:58:29 +04:00
Sivin VargheseandGitHub a3d05ef55d chore: Update emoji picker in widget (#14741) 2026-06-16 10:39:47 +05:30
dda25c0b51 fix(portal): emit valid BCP 47 html lang attribute (#14680)
Locales are stored with underscores (e.g. pt_BR), but the HTML lang
attribute requires hyphens (pt-BR). Convert via a helper in the public
portal layouts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-15 14:34:52 -07:00
Sivin VargheseandGitHub 1713cabdf2 feat: Add emoji & icon picker for Help Center categories (#14702) 2026-06-16 02:10:05 +05:30
d8a16278b9 fix: added ordering capability for sidebar sections folders, teams, channels and labels (CW-7193) (#14609)
## Description

* Added the ability to sort for 4 sub-sections under conversations
folders, teams, channels and labels.
* the sort options are basically created at, alphabetical and unread
counts along with both directions.
* for folders we don't have an unread count, so we sort it by only
created and alphabetical.
* all the sort preferences are stored on the frontend - easiest
implementation for now.

Fixes # CW-7193

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested this locally by visually verifying the changes. Also ran the
newly added tests for component level changes.

Here is the screenshot of the changes:

Added the sort option to sub sections in the conversation sidebar:
<img width="282" height="848" alt="Screenshot 2026-06-02 at 1 58 12 AM"
src="https://github.com/user-attachments/assets/4a7c6061-86e3-438a-92ae-ee643a0128b6"
/>

The sort options looks like this:
<img width="783" height="698" alt="Screenshot 2026-06-02 at 1 58 49 AM"
src="https://github.com/user-attachments/assets/a15bb0a7-b810-4423-a88c-fbd84d0476c0"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-06-15 21:34:19 +05:30
Sony MathewandGitHub 4816e923b6 chore: added a new sort by unread option for conversations (CW-7152) (#14512)
## Description

Added a new option for the sort by option in conversation filters called
unread. This is to filter out unread conversations.
Fixes # CW-7152

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested manually in local.

<img width="1397" height="603" alt="Screenshot 2026-05-20 at 10 40
20 PM"
src="https://github.com/user-attachments/assets/6c60263e-907b-419f-a7ed-06010bbe8736"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-15 17:58:04 +05:30
396631ad7d feat: enforce concurrent session limit with login picker (CW-7169) (#14621)
## Description

Cap active sessions at `MAX_USER_SESSIONS` which defaults to existing
value of `25` per user. This ensure existing user login behavior is not
affected for self-hosted installations. Browser users at the cap see a
session picker (409 response) to choose which session to end.
Non-browser clients and partially-tracked users get silent
oldest-session eviction.

Depends on #14556.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Specs cover: under limit, at limit (browser picker, non-browser
eviction), partial tracking fallback, revoke single/all sessions during
login, session row creation on successful login.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-15 17:28:13 +05:30
Vishnu NarayananandGitHub ba0ba46c9c fix: skip session tracking and use short-lived token for impersonation (CW-7169) (#14622)
## Description

SuperAdmin impersonation SSO logins no longer create UserSession rows
visible to the customer. Impersonation tokens use a 2-day lifespan
instead of ~2 months, so they naturally evict first and don't linger in
the user's token list.

Server-side detection via Redis value (`'impersonation'` vs `'normal'`)
without changing the `valid_sso_auth_token?` signature. Backward
compatible with in-flight tokens.

Depends on #14556.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Specs cover: impersonation login skips UserSession creation,
impersonation token has short lifespan, normal SSO login still creates
UserSession row.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-06-15 17:14:34 +05:30
Vishnu NarayananandGitHub ee6382109a feat: prevent deleted email conversations from syncing again (#14612)
# Pull Request Template

## Description

Prevent deleted email conversations from being synced into Chatwoot
again while they are still within the IMAP sync window.

When an admin explicitly deletes an email conversation, the incoming
email message IDs are stored temporarily in Redis. IMAP sync checks
these recently deleted message IDs in addition to existing message
records. Each Redis key expires automatically after two days.

This applies only to explicit conversation deletion. Individual message
deletion, inbox deletion, and account deletion keep their existing
behavior.

Fixes
[CW-7214](https://linear.app/chatwoot/issue/CW-7214/deleted-mails-in-gmail-inbox-gets-synced-again)
2026-06-15 17:06:40 +05:30
Sivin VargheseandGitHub 35bef21f83 feat: add media view for contacts (#14393) 2026-06-15 15:42:11 +05:30
e5c140158e fix(new-conversation): stop writing literal "undefined" as mail_subject (#14383)
## Description

This PR fixes a data-corruption bug in the new conversation flow that
surfaces in conversation search results as `Subject: undefined`.

### The Problem
When creating a conversation through the "New conversation" modal
without typing a subject (every non-email channel never shows the
subject input, and email channels can be left blank), the conversation
gets persisted with `additional_attributes.mail_subject = "undefined"`
(the literal string). The bogus value shows up in conversation search
results as `Subject: undefined`, which started rendering after #10843.

### Root Cause
`createConversationPayload` in
`app/javascript/dashboard/store/modules/contactConversations.js` appends
the `mail_subject` field unconditionally:

```js
payload.append('additional_attributes[mail_subject]', mailSubject);
```

`composeConversationHelper.js` only sets `payload.mailSubject` when
`subject` is truthy, so when no subject is provided, `mailSubject` is
destructured as `undefined` in `createConversationPayload`.
`FormData.append` coerces `undefined` to the string `"undefined"`, and
the backend persists it as-is into the JSONB column.

### The Fix
Skip the append when `mailSubject` is falsy. The backend already treats
a missing key the same as an empty subject (the email mailer falls back
to a default subject when `mail_subject` is `nil`), so omitting the
field is safe across channels.

### Key Changes
- Guarded the `additional_attributes[mail_subject]` append in
`createConversationPayload`.
- Added a spec covering the case where `mailSubject` is omitted.

> Note: existing rows persisted before this fix will continue to show
`Subject: undefined` in search until cleaned up. A simple SQL cleanup
for non-email inboxes:
>
> ```sql
> UPDATE conversations
> SET additional_attributes = additional_attributes - 'mail_subject'
> FROM inboxes
> WHERE conversations.inbox_id = inboxes.id
>   AND inboxes.channel_type <> 'Channel::Email'
> AND conversations.additional_attributes->>'mail_subject' =
'undefined';
> ```
>
> I'm leaving any data-cleanup migration out of this PR since it's a
maintenance concern that may be handled differently per installation.

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

1. Open the "New conversation" modal on a non-email inbox (e.g.,
WhatsApp, SMS, API).
2. Create a conversation without filling any subject (field is not
present on those, so regular flow).
3. Open the global search and search for the conversation.
4. **Before:** the result card shows `Subject: undefined`.
   **After:** the subject row is hidden.
5. Repeat on an email inbox leaving the subject blank — same result.
6. On an email inbox, type a subject and verify it still persists and
renders correctly.
7. Run the new spec: `pnpm test contactConversations`.

## Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [X] My changes generate no new warnings
- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-14 08:57:38 +05:30
274e92e0e4 chore: collapse conversation sidebar sections (folders, teams, inboxes and labels) - CW-7059 (#14509)
## Description

Added ability to collapse conversation sidebar sections (folders, teams,
inboxes and labels)

Fixes #CW-7059

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested locally. Added specs. Attaching the loom for them same.


https://github.com/user-attachments/assets/40d613e7-6c82-4078-abf4-79739a00f718


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-06-14 06:54:18 +05:30
27404b4a27 fix: redact sensitive integration secrets from API responses (#14147)
## Summary

The `GET /api/v1/accounts/:id/integrations/apps` endpoint returns raw
secret values (OpenAI API keys, Google service account private keys,
Linear refresh tokens, etc.) in hook settings within the JSON response.
Although gated behind an administrator check, these secrets are visible
in the browser network tab.

This PR filters hook settings through the existing `visible_properties`
whitelist defined in `config/integration/apps.yml`, and adds explicit
whitelists to integrations that were missing them.

Closes #14042

## Bug reproduction

**Setup:** Created an OpenAI integration hook with a fake API key
(`sk-test-secret-12345`).

**Step 1 — Browser Network tab shows raw secrets:**

<img width="1676" height="869" alt="Screenshot 2026-04-24 at 14 32 28"
src="https://github.com/user-attachments/assets/6fc69463-365e-458b-b216-98a7390bf528"
/>


Navigate to Settings > Integrations as an admin. Open DevTools Network
tab and observe the response from `GET
/api/v1/accounts/:id/integrations/apps`. The hook settings contain the
full API key in plaintext:

```json
"hooks": [
  {
    "id": 1,
    "app_id": "openai",
    "settings": {
      "api_key": "sk-test-secret-12345",
      "label_suggestion": false
    }
  }
]
```

**Step 2 — API call confirms the leak:**

```bash
curl -s "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "access-token: <token>" \
  -H "client: <client>" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
```

Response:

```json
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "api_key": "sk-test-secret-12345",
        "label_suggestion": false
      }
    }
  ]
}
```

Any admin user can extract the raw API key from the response. The same
applies to other integrations — Dialogflow exposes full Google service
account credentials, Linear exposes refresh tokens, etc.

## After fix

After applying this change, the GET
/api/v1/accounts/:id/integrations/apps endpoint no longer returns
sensitive secret values in hook settings. Instead, the response is
filtered using each integration’s visible_properties whitelist, ensuring
only safe, user-facing fields are exposed. For example, OpenAI
integrations return non-sensitive fields like label_suggestion while
excluding raw API keys. This prevents secrets from being exposed in the
browser network tab or API responses, even for authenticated admin
users.

```bash
curl -X GET "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer eyJhY2Nlc3MtdG9rZW4iOiJqZG5jOWg4TnljaWFJa3JlZkxGQzRnIiwidG9rZW4tdHlwZSI6IkJlYXJlciIsImNsaWVudCI6Ik81bjVnTDFEOVVhbGpwbWxjaHZNanciLCJleHBpcnkiOiIxNzgyMzMyNTA4IiwidWlkIjoidXNlckB0ZXN0LmNvbSJ9" \
  -H "access-token: jdnc9h8NyciaIkrefLFC4g" \
  -H "client: O5n5gL1D9UaljpmlchvMjw" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10174  100 10174    0     0  33232      0 --:--:-- --:--:-- --:--:-- 33357
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "label_suggestion": false
      }
    }
  ]
}
```

## What changed

- Added `visible_properties` accessor to `Integrations::App` model to
expose the whitelist from config
- Updated `_hook.json.jbuilder` to filter `resource.settings` through
the associated app's `visible_properties` instead of returning the full
hash
- Added `visible_properties` to integrations that were missing it:
  - Linear: `[]` (settings contain refresh_token)
  - Notion: `[]` (OAuth-based, no user-facing settings)
- Slack: `['channel_name']` (UI needs this to display connected channel)
  - Shopify: `[]` (no settings)

App config metadata (`_app.json.jbuilder`) is left unchanged —
hook_type, settings_form_schema, etc. are not secrets and the frontend
depends on them.

## How to test

1. Create an OpenAI integration hook with an API key
2. As an admin, call `GET /api/v1/accounts/:id/integrations/apps`
3. Verify hook settings include `api_key` (whitelisted) but not raw
credential objects
4. For Dialogflow hooks, verify `credentials` (private key JSON) is
excluded while `project_id` is included
5. For Slack hooks, verify `channel_name` is still returned
6. For Linear hooks, verify `refresh_token` is not returned

---------

Co-authored-by: Botshelo Nokoane (Konstruktors) <botshelo@entersekt.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-14 06:53:13 +05:30