\`GET /platform/api/v1/agent_bots\` returns 500 when any \`AgentBot\`
that was previously registered with a Platform App has since been
deleted. The bug was introduced by a missing \`dependent: :destroy\` on
the \`AgentBot\` model — deleting a bot left orphaned rows in
\`platform_app_permissibles\`, which the index action later iterated
over and crashed rendering with a \`NoMethodError\` on \`nil\`.
Closes#13407
## Root cause
The index action loads all \`platform_app_permissibles\` for the
platform app and passes each \`resource.permissible\` (the associated
\`AgentBot\`) to a Jbuilder partial. When the \`AgentBot\` no longer
exists, \`resource.permissible\` returns \`nil\` and the partial crashes
calling \`.id\`, \`.name\`, etc. on it.
Every other \`AgentBot\` association (\`agent_bot_inboxes\`,
\`messages\`, \`assigned_conversations\`) had a \`dependent:\` option —
\`platform_app_permissibles\` was the only one missing it. There was
also an N+1 query: the index fired a separate SQL query per permissible
to load each bot.
## What changed
**1. Model — prevent orphans at deletion time**
\`\`\`ruby
has_many :platform_app_permissibles, as: :permissible, dependent:
:destroy
\`\`\`
**2. Controller — eager-load to eliminate N+1**
\`\`\`ruby
@resources = @platform_app.platform_app_permissibles
.where(permissible_type: 'AgentBot')
.includes(:permissible)
\`\`\`
**3. Jbuilder — defensive nil guard for pre-existing orphans**
\`\`\`ruby
bot = resource.permissible
next if bot.nil?
json.partial! '...', resource: bot
\`\`\`
## Trade-offs considered
| Option | Decision |
|---|---|
| Rescue \`NoMethodError\` in jbuilder | Hides the failure rather than
fixing it. Rejected. |
| Only add the nil guard, skip the model fix | Leaves the data integrity
gap open — future deletions continue creating orphans. Rejected. |
| Both layers (chosen) | Model fix prevents new orphans; nil guard is
defence-in-depth for any orphans that survived before deployment. |
| \`dependent: :nullify\` | Doesn't apply — a nullified permissible
would still cause the same nil dereference. Rejected. |
## How to reproduce
1. Create an AgentBot via the Platform API
2. Delete the AgentBot via any path (admin UI, API, or direct model
call)
3. Call \`GET /platform/api/v1/agent_bots\` with a Platform App token
4. Observe 500
After this fix, the endpoint returns 200 with an empty array.
Co-authored-by: Ramalau Debeila <rdebeila@datacentrix.co.za>
Standardizes the contact company import/filter/automation contract on
`company_name`.
Closes#14096
Revives #9907
## Why
Contact company is read across the current CRM/contact UI from
`additional_attributes['company_name']`, but CSV import and a few
backend filter/automation paths still used the older `company` key. That
meant imported company values could be saved in a place the dashboard,
sorting, filters, and automation conditions did not consistently read
from.
Based on the production data check, the legacy `company` automation
configuration is effectively dead: the affected account did not have
contacts populated with `additional_attributes['company']`. So this PR
intentionally avoids adding long-term fallback behavior and uses
`company_name` as the single key going forward.
## What changed
- Contact CSV import now writes only `company_name` into
`additional_attributes['company_name']`.
- The example contact import CSV now uses the `company_name` header.
- Contact company sorting/filter config now uses `company_name`.
- Automation condition config now uses `company_name`.
- Existing standard automation conditions with `attribute_key:
'company'` are migrated to `company_name`.
- Existing saved contact filters with standard `attribute_key:
'company'` are migrated to `company_name`.
- Custom attributes named `company` are preserved and are not rewritten
by the migration.
## How to test
- Import a contact CSV with a `company_name` column and confirm the
Contact Company field is populated.
- Sort contacts by Company and confirm imported contacts are ordered
correctly.
- Create/edit an automation with Company as a condition and confirm it
saves with `company_name`.
- Verify existing saved contact filters and automation rules using the
old standard `company` key are migrated to `company_name`.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
# Pull Request Template
## Description
Captain currently cannot discern today, tomorrow etc. This PR adds
datetime awareness to the system prompt
Fixes:
https://linear.app/chatwoot/issue/AI-148/captain-should-be-aware-of-datetime
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## 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
<img width="696" height="247" alt="CleanShot 2026-04-27 at 14 47 47"
src="https://github.com/user-attachments/assets/6a73a8d9-f48e-46bb-a306-7b9a28a5fa9c"
/>
## 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
Loads Rails locale-specific pluralization rules so languages with an
`other`-only plural model can safely use Crowdin exports without
maintaining duplicate `one` keys.
## Closes
None
## Why
Crowdin exports Rails YAML pluralized strings using each target
language's plural categories. These categories come from Unicode CLDR
and represent grammatical forms, not a literal "number is 1" bucket.
Some languages need separate forms such as `one` and `other`, but
languages like Japanese, Korean, Indonesian, Thai, Vietnamese, and
Chinese use the same form for `1`, `2`, `5`, and larger counts in these
strings. For those locales, CLDR correctly models the plural category as
`other` only.
Before this change, Chatwoot still relied on Rails' default
English-style plural behavior for these locales. That meant a valid
Crowdin export containing only `other` could fail at runtime when Rails
received `count: 1` and looked for a missing `one` branch.
Keeping duplicate `one` keys would only fight Crowdin on every
translation sync. The runtime should instead follow the locale's plural
rules.
## What changed
- Added `rails-i18n` and enabled only its pluralization module.
- Added explicit `other`-only plural rules for Chatwoot's underscore
Chinese locale aliases, `zh_CN` and `zh_TW`.
- Removed redundant `one` keys from the affected Devise and `time_units`
translations.
## Validation
- Ran a Rails runner check across `id`, `ja`, `ko`, `ms`, `th`, `vi`,
`zh_CN`, and `zh_TW` to verify `errors.messages.not_saved` and
`time_units.days` resolve with only `other` for `count: 1`.
- Ran YAML parse validation for all edited locale files.
- Ran `bundle exec rubocop Gemfile config/application.rb
config/initializers/i18n_pluralization.rb`.
# Pull Request Template
## Description
This PR fixes an issue where signature images (with
`?cw_image_height=...`) render at their original large size in the email
bubble.
### Cause
Renderer output:
```html
<img src="..." height="24px" width="auto" />
```
Email UI and clients (Gmail, Outlook) apply CSS like:
`img { max-width: 100%; height: auto; }`
This overrides `height="24px"`.
Other channels work because they use inline styles (`style="height:
24px;"`).
### Solution
Use inline style instead:
```html
<img src="..." style="height: 24px;" />
```
### Why backend fix
* Fixes root cause and aligns Ruby + JS renderers
* Works in both Chatwoot UI and recipient inboxes
* Covers all email-rendered content
* Minimal change
Fixes
https://linear.app/chatwoot/issue/CW-6948/email-signature-image-renders-oversized-in-chatwoot-ui
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
#### Screenshots
**Before**
<img width="1637" height="377" alt="image"
src="https://github.com/user-attachments/assets/0477f6fb-3b95-4fc3-9ea8-f59b71e27f47"
/>
**After**
<img width="1637" height="289" alt="image"
src="https://github.com/user-attachments/assets/de5ea4c1-8452-4c5f-aeb1-e1e11e0fe7d5"
/>
## 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
# Pull Request Template
## Description
When creating a help center article, typing a title and navigating into
the content auto-creates the article and switches the route
(`/articles/new` → `/articles/.../edit/:slug`). During this transition,
focus was jumping back to the title, interrupting editing.
This happened because `ArticleEditor` always autofocuses the title. On
route change, the component remounts and re-triggers focus. Now, after
auto-create, focus stays in the body as expected.
Fixes
https://linear.app/chatwoot/issue/CW-6951/issue-with-the-cursor-position-on-the-help-center-article-when
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
**Screencast**
https://github.com/user-attachments/assets/dac3f7c6-08c4-4df2-afb0-7731ee76424b
## 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
# Pull Request Template
## Description
- Fetch main content only from Firecrawl, exclude some tags to remove
boilerplate
- Prompt changes for FAQ generation
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
## 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.
tested locally
## 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
Adds the ability to translate help center articles to other languages using Captain's LLM infrastructure. Translated articles are created as drafts linked to the source article.
Fixes
https://linear.app/chatwoot/issue/CW-6901/translate-article-to-another-language
**How to test**
1. Navigate to Help Center → Articles for a portal with multiple locales
2. Click the three-dot menu on any article → "Translate"
3. Select a target language and category → click Translate
4. Switch to the target locale — the translated article appears as a
draft
5. Try translating the same article again — a warning shows the existing
translation with a link to open it in a new tab
6. Click "Overwrite and translate" to replace the existing translation
https://github.com/user-attachments/assets/1d2e991b-f0ac-403a-bcc1-2181b5731ea4
This routes external downloads used by avatar sync through SafeFetch. It closes the SSRF exposure from raw Down.download paths, preserves provider-specific auth and header flows, and adds regression coverage
for blocked internal URLs plus authenticated downloads.
Fixes # (issue): [CW-6931](https://linear.app/chatwoot/issue/CW-6931/avatarwidget-url-ssrf-downdownload-unprotected-unauth)
## Description
Spreadsheet applications such as Microsoft Excel do not auto-detect
UTF-8 encoding when opening CSV files. This causes non-ASCII characters
(Arabic, Japanese, Chinese, Korean, etc.) to appear garbled in the
exported contacts CSV.
This PR prepends the UTF-8 Byte Order Mark (`EF BB BF`) to the CSV
output in `Account::ContactsExportJob`, which signals to spreadsheet
applications that the file is UTF-8 encoded.
Fixes: #13998
## Description
`DataImportJob#csv_reader` reads CSV data with `force_encoding('UTF-8')`
but does not strip the UTF-8 Byte Order Mark (`EF BB BF`). If a CSV file
containing a BOM is imported, the first header key is prefixed with
`\uFEFF`, which causes key mismatches in `DataImport::ContactManager`
when the first column is one of the recognized keys (`:email`,
`:identifier`, `:phone_number`, `:name`).
This was identified during review of #14123 (see #14124 for the tracking
issue).
Fixes#14124
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added a new fixture (`spec/fixtures/data_import/with_bom.csv`)
containing a UTF-8 BOM followed by valid contact data.
- Added a new spec (`will strip UTF-8 BOM and import contacts
correctly`) that imports the BOM fixture and verifies that `name`,
`email`, and `phone_number` are all correctly parsed.
- All existing examples in `spec/jobs/data_import_job_spec.rb` continue
to pass.
## 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] 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
# Pull Request Template
## Description
Document auto-sync job pipeline without wiring to controllers or FE
Fixes # (issue)
## 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.
specs and locally
## 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
## Summary
Outbound email messages never populate
`content_attributes.email.subject`. The subject lives only on the parent
conversation's `additional_attributes.mail_subject`. The search index
doc built by `Messages::SearchDataPresenter` pulls from the
message-level field only, so outbound email subjects are unsearchable
for accounts on the advanced_search (Elasticsearch) path.
This change makes the presenter fall back to
`conversation.additional_attributes.mail_subject` when the message-level
subject is blank. Inbound email messages keep their existing behavior
(message-level subject takes precedence).
Closes https://linear.app/chatwoot/issue/CW-6877
## What changes
- `Messages::SearchDataPresenter#content_attributes_data` now falls back
to the conversation's `mail_subject` when the message-level subject is
blank.
- Added specs covering the fallback, precedence, and the neither-set
case.
## What does not change
- No schema changes, no migrations, no backfill of existing OpenSearch
documents.
- Searchkick's `after_commit :reindex_for_search` on `Message` will pick
up the new field for all newly created or updated messages via the
existing indexing path.
- Postgres search path (free accounts, fallback) is untouched. Broader
subject search for those users is a separate follow-up.
When a credit top-up's card charge fails, the finalized invoice was left
in an open state with no hook back into our fulfillment service. If that
invoice was later paid (manually via the hosted invoice page, or by a
future dunning flow), the account would never receive its credits —
silent revenue loss.
This change voids the invoice the moment the charge fails, so a failed
top-up cannot turn into a paid-but-unfulfilled invoice.
## Closes
<!-- add the relevant issue / Linear link -->
## How to test
1. On a Business-plan account with a Stripe customer, attach a test card
that will decline on charge (e.g. `4000 0000 0000 0002`).
2. From the dashboard, open the credit top-up flow and purchase a credit
pack.
3. Observe the API returns an error and the account's captain credits
are unchanged.
4. In the Stripe dashboard, confirm the corresponding invoice is in
`void` status (not `open`).
5. Repeat with a good card (`4242 4242 4242 4242`) and confirm the happy
path still fulfills credits.
## What changed
- `finalize_and_pay` in `Enterprise::Billing::TopupCheckoutService` now
rescues `Stripe::CardError`, voids the open invoice via
`Stripe::Invoice.void_invoice`, and re-raises so the controller surfaces
the original decline error to the client.
- Rescue is intentionally narrow to `Stripe::CardError` (declines,
insufficient funds, SCA `authentication_required`). Transient errors
like `APIConnectionError` / `RateLimitError` are left to propagate — the
charge may have actually succeeded and voiding could be wrong.
- Invoices are created with `auto_advance: false`, so Stripe's Smart
Retries won't collect on an open invoice; voiding is the correct
terminal state.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
We're getting many customer reports saying "I'm not getting
notifications." We can't always identify the root cause since there are
multiple points of failure. Added a **Push Diagnostics** tool in Super
Admin to help us investigate mobile/web push issues.
Here's how it works:
- Look up a user by email/ID → see all their registered subscriptions
with device info (iOS/Android, brand, model), token freshness, and
last-updated time
- Send a customizable test push and read the raw FCM/web-push/relay
response to see if the customer is receiving push notifications—if not,
it will show proper errors.
- Delete broken subscriptions so the mobile app re-registers on next
launch
<img width="3816" height="1974" alt="CleanShot 2026-04-20 at 12 56
56@2x"
src="https://github.com/user-attachments/assets/08ecab6f-7ec3-44b3-a114-5e6eb8cf0879"
/>
Fixes https://linear.app/chatwoot/issue/CW-6892/push-diagnostics-tool
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#13619
## Summary
- Add `TwilioSignatureVerifyConcern` that validates the
`X-Twilio-Signature` header using `Twilio::Security::RequestValidator`
(already bundled via `twilio-ruby` gem)
- Include the concern in `Twilio::CallbackController` and
`Twilio::DeliveryStatusController` — both endpoints were previously
accepting requests from any source with no authentication
- Channels using API key authentication (`api_key_sid` present) skip
validation with a warning log, since Twilio signs with the account auth
token which isn't stored for those channels
## How it works
1. `before_action` looks up the `Channel::TwilioSms` from request params
(`MessagingServiceSid` or `AccountSid` + phone number)
2. Validates the HMAC-SHA1 signature using the channel's auth token
3. Returns `403 Forbidden` if signature is invalid, missing, or channel
not found
4. Handles reverse proxy URL reconstruction via `X-Forwarded-Proto`
header
Follows the same pattern used by `Webhooks::ShopifyController` and
`Webhooks::TiktokController`.
## Test plan
- [x] Valid signature → 204 No Content, job enqueued
- [x] Invalid signature → 403 Forbidden, job not enqueued
- [x] Missing signature header → 403 Forbidden
- [x] Channel not found → 403 Forbidden
- [x] API key channel → skips validation, job enqueued (with warning
log)
- [x] MessagingServiceSid lookup → validates and enqueues
- [x] All existing Twilio service/job specs pass (99 examples, 0
failures)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Description
When an inbox name or business name contains parentheses — e.g. `Giro
Crédito - Soporte (Email` — the resulting From header becomes
unparseable by SMTP servers. The `(` is interpreted as an RFC 5322
comment start, swallowing the actual email address and causing a `553
Invalid email address` rejection.
Closes [CW-6323](https://linear.app/chatwoot/issue/CW-6323)
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How to reproduce?
1. Set an inbox's name or business name to include a parenthesis, e.g.
`Support (Email`
2. Send an outgoing email reply from that inbox
3. Observe `Net::SMTPFatalError: 553 ... Invalid email address`
## 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
# Pull Request Template
## Description
This PR includes,
On Windows, pressing **Ctrl+Enter** in the reply editor was inserting an
unintended line break before sending. This led to two issues:
* **Unexpected blank lines**
After adding a line break with Shift+Enter and removing it with
Backspace, the editor looked correct. However, sending with Ctrl+Enter
reintroduced a hidden break, resulting in an extra blank line in the
final message.
* **Selected text being replaced**
When text was selected and Ctrl+Enter was pressed, the selection was
replaced with a line break instead of being sent.
Fixes
https://linear.app/chatwoot/issue/CW-6840/newline-bug-in-the-editor
### **Cause**
Two keyboard handlers responded to **Ctrl+Enter** on Windows:
* ProseMirror (`Mod-Enter`) inserted a hard break
* ReplyBox (`$mod+Enter`) triggered send
The existing guard only checked `metaKey` (Cmd), so it never worked on
Windows. As a result, a line break was inserted just before sending.
### **Solution**
Make the modifier check platform-aware so the editor correctly
intercepts the send shortcut:
* Added `detectOS`, `isMac`, and `OS` constants
* Introduced `hasPressedMod` (uses `metaKey` on macOS, `ctrlKey`
elsewhere)
This ensures Ctrl+Enter sends the message without modifying content,
while keeping existing behavior unchanged.
**NB:** macOS behavior with Cmd+Enter remains unchanged
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
**Case 1: line break**
1. Type `hello`
2. Press Shift+Enter, then Backspace
3. Press Ctrl+Enter
→ Message contains an unexpected blank new line
**Case 2: Selection replaced**
1. Type two lines using Shift+Enter
2. Select text on the second line
3. Press Ctrl+Enter
→ Selected text is replaced and not sent
### Screencast
**Before**
https://github.com/user-attachments/assets/d6d285a9-260b-4711-8bbd-d0c8519e8d20
**After**
https://github.com/user-attachments/assets/c0ace1f7-5d22-44a2-8e08-22190ee21e61
## 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
The "conversation continuity via email" toggle was visible to all
accounts regardless of whether they had `inbound_emails` enabled.
Without inbound email infrastructure, replies to those follow-up emails
land in the agent's personal inbox instead of routing back into
Chatwoot. The feature appears to work but silently breaks the reply
path.
The toggle is now gated on the `inbound_emails` feature flag. On
self-hosted without the feature, the toggle is hidden entirely. On
cloud, it remains visible but disabled with upgrade messaging.
On the backend, `inbound_emails` is added to the manually managed
features list in `InternalAttributesService` so that Stripe webhook plan
syncs don't override it when support enables it for an account.
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
## PR2: Report builder refactor — DataSource abstraction
The existing report builders (timeseries + summary) had their SQL
queries inlined — each builder constructed its own scopes, groupings,
and aggregations directly. This made it hard to swap the underlying data
source without duplicating builder logic.
This PR extracts all raw-event querying into a `Reports::RawDataSource`
behind a `Reports::DataSource` factory. Builders now call
`data_source.timeseries`, `.aggregate`, or `.summary` instead of
constructing queries themselves. Behavior is identical —
`DataSource.for(...)` returns `RawDataSource` in all cases today.
The timeseries path had two separate builders (`CountReportBuilder`,
`AverageReportBuilder`) that were selected via a metric-name case
statement in `Conversations::BaseReportBuilder`. These are replaced by a
single `ReportBuilder` that delegates to the data source. The metric
type (count vs average) is now decided inside the data source, not the
builder.
Summary builders similarly moved their inline SQL into
`RawDataSource#summary`, which returns a unified hash keyed by dimension
ID.
the rollup read path.
## Flow
### Before
```
ReportsController ──▶ case metric ──▶ AverageReportBuilder ──▶ inline SQL ──▶ DB
└──▶ CountReportBuilder ──▶ inline SQL ──▶ DB
SummaryController ──▶ AgentSummaryBuilder ──▶ inline SQL ──▶ DB
└──▶ InboxSummaryBuilder ──▶ inline SQL ──▶ DB
└──▶ TeamSummaryBuilder ──▶ inline SQL ──▶ DB
```
### After
```
ReportsController ──▶ ReportBuilder ──┐
├──▶ DataSource.for ──▶ RawDataSource ──▶ DB
SummaryController ──▶ SummaryBuilder ──┘
```
### Expected (after rollup read path)
```
ReportsController ──▶ ReportBuilder ──┐
├──▶ DataSource.for ──▶ RawDataSource ──▶ reporting_events
SummaryController ──▶ SummaryBuilder ──┘ └──▶ RollupDataSource ──▶ reporting_events_rollups
```
### What changed
- `Reports::DataSource` factory + `Reports::RawDataSource`
- `TimezoneHelper#timezone_name_from_params` — prefers IANA name, falls
back to offset
- Unified `Timeseries::ReportBuilder` replaces `CountReportBuilder` +
`AverageReportBuilder`
- Summary builders delegate to `DataSource` instead of querying directly
### How to test
This is a pure refactor — all existing report pages (Overview, Agent,
Inbox, Label, Team) should produce identical numbers. No feature flag or
new config needed.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
Introduce a `Last Responding Agent` options to assign_agents action in
automations to cover the following use cases.
- Assign conversations to first responding agent : ( automation message
created at , if assignee is nil, assign last responding agent )
- Ensure conversations are not resolved with out an assignee : (
automation conversation resolved at : if assignee is nil, assign last
responding agent )
and potential other cases.
fixes: #1592
## Summary
The `assignee_name` and `user_name` variables are swapped in the Chinese
(zh/zh_CN) locale files for conversation assignment activity messages,
causing the rendered text to show the wrong person as the assignee.
### Before (incorrect)
| Template | English (correct) | Chinese (incorrect) |
|---|---|---|
| `assignee.assigned` | Assigned to **AgentA** by **Admin** | 由
**AgentA** 分配给 **Admin** |
| `team.assigned` | Assigned to **TeamX** by **Admin** | 由 **TeamX** 分配给
**Admin** |
| `team.assigned_with_assignee` | Assigned to **AgentA** via **TeamX**
by **Admin** | 由 **AgentA** 分配给 **TeamX** 团队的 **Admin** |
The Chinese text reads as if the conversation was assigned **to Admin**
(the API caller), when it was actually assigned **to AgentA**.
### After (correct)
| Template | Chinese (fixed) |
|---|---|
| `assignee.assigned` | 由 **Admin** 分配给 **AgentA** |
| `team.assigned` | 由 **Admin** 分配给 **TeamX** |
| `team.assigned_with_assignee` | 由 **Admin** 通过 **TeamX** 团队分配给
**AgentA** |
Now correctly matches the English template semantics.
## Files Changed
- `config/locales/zh_CN.yml` — 3 lines
- `config/locales/zh.yml` — 3 lines
## How to Verify
1. Set locale to `zh_CN`
2. Have an admin assign a conversation to an agent
3. Check the activity message in the conversation — the assignee name
should appear after "分配给", not before it
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This updates macros and automations so agents can explicitly remove
assigned agents or teams, while keeping the existing `Assign -> None`
flow working for backward compatibility.
Fixes: #7551Closes: #7551
## Why
The original macro change exposed unassignment only through `Assign ->
None`, which made macros behave differently from automations and left
the explicit remove actions inconsistent across the product. This keeps
the lower-risk compatibility path and adds the explicit remove actions
requested in review.
## What this change does
- Adds `Remove Assigned Agent` and `Remove Assigned Team` as explicit
actions in macros.
- Adds the same explicit remove actions in automations.
- Keeps `Assign Agent -> None` and `Assign Team -> None` working for
existing behavior and stored payloads.
- Preserves backward compatibility for existing macro and automation
execution payloads.
- Downmerges the latest `develop` and resolves the conflicts while
keeping both the new remove actions and current `develop` behavior.
## Validation
- Verified both remove actions are available and selectable in the macro
editor.
- Verified both remove actions are available and selectable in the
automation builder.
- Applied a disposable macro with `Remove Assigned Agent` and `Remove
Assigned Team` on a real conversation and confirmed both fields were
cleared.
- Applied a disposable macro with `Assign Agent -> None` and `Assign
Team -> None` on a real conversation and confirmed both fields were
still cleared.
# Pull Request Template
## Description
This PR adds support for resizing the reply editor up to nearly half the
screen height. It also deprecates the old modal-based pop-out reply box,
clicking the same button now expands the editor inline. Users can adjust
the height using the slider or the expand button.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/be27e1c06d19475ab404289710b3b0da
## 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
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Update BulkSelectBar to compute selection state (indeterminate/all) from
visible item IDs and only toggle selection for visible items. Preserve
existing selection for off-screen items when toggling, and guard against
empty visibility. Add detection/rendering for an optional
secondary-actions slot and adjust layout/divider. Also fix
ContactsBulkActionBar selection logic to determine "all selected" by
verifying every visible ID is in the selection. These changes ensure
correct select-all behavior with filtered/visible lists and support
additional UI actions.
https://github.com/user-attachments/assets/d06b78d1-a64a-4c0c-a82a-f870140236c7
# Pull Request Template
## Description
Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] 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?
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.
## 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: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
RubyLLM bundles a static models.json that doesn't know about models
released after the gem was published. Self-hosted users configuring
newer models hit ModelNotFoundError.
Added a rake task that refreshes the registry from models.dev and saves
to disk. ~~Called during Docker image build so every deploy gets fresh
model data. Falls back silently to the bundled registry if models.dev is
unreachable.~~
Commit the models.json file to code so it is available across
deployments.
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
# Pull Request Template
## Description
Add migrations for document auto-sync
Fixes # (issue)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
locally
## 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
## Summary
This PR enables the **Participating** conversation view in the main
sidebar and keeps the behavior aligned with existing conversation views.
## What changed
- Added **Participating** under Conversations in the new sidebar.
- Added a guard in conversation realtime `addConversation` flow so
generic `conversation.created` events are not injected while the user is
on Participating view.
- Added participating route mapping in conversation-list redirect helper
so list redirects resolve correctly to `/participating/conversations`.
## Scope notes
- Kept changes minimal and consistent with current `develop` behavior.
- No additional update-event filtering was added beyond what existing
views already do.
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
## Linear ticket
https://linear.app/chatwoot/issue/CW-6839/blocked-contact-can-still-send-messages-to-whatsapp-inbox
## Description
Drop WhatsApp incoming messages from blocked contacts
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Incoming messages for blocked contacts
## 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: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
This PR adds inline editing support for contact name, phone number,
email, and company fields in the conversation contact sidebar
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
**Screencast**
https://github.com/user-attachments/assets/e9f8e37d-145b-4736-b27a-eb9ea66847bd
## 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
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>