* fix(conversations): enforce NOT NULL + FK on contact_id
Conversations had contact_id nullable with no FK to contacts. Combined
with dependent: :destroy_async on Contact#conversations, deleting a
contact could leave conversations pointing to a missing contact,
breaking the conversations#index API with
"undefined method 'additional_attributes' for nil" from the contact
partial.
Changes:
- Migration cleans up existing orphans, sets contact_id NOT NULL and
adds a FK with ON DELETE CASCADE so the invariant is enforced at the
DB level (complements the existing Rails presence validation).
- ContactMergeAction uses update_all for conversations/messages/notes/
contact_inboxes so a failing callback cannot silently leave records
pointing to the mergee contact before it is destroyed.
- Drop the now-redundant orphan filter in Conversations::ResolutionJob
and its spec; the invariant is enforced at the schema level.
* fix: address review feedback
- Drop the ON DELETE CASCADE FK on conversations.contact_id. Several
conversation-owned tables (messages, mentions, conversation_participants,
reporting_events, csat_survey_responses, calls, applied_slas, sla_events,
and polymorphic notifications) still have plain conversation_id references
without FK cascades. The DB-level cascade would skip Conversation's
dependent: cleanup and replace the NULL-contact bug with orphan children,
and would also conflict with the existing non-cascade FKs on
scheduled_messages/recurring_scheduled_messages. Keep the invariant at the
Rails layer (NOT NULL + presence validation + dependent: :destroy_async).
- Clean up orphan conversations in the migration via Rails destroy so
dependent associations are propagated correctly, instead of a raw
DELETE FROM conversations that would orphan all child rows.
- Revert ContactMergeAction.merge_* methods back to per-record update! so
Conversation#after_update_commit still fires (notify_status_change /
CONVERSATION_CONTACT_CHANGED) for contact_id changes. The bang form
still removes the silent-failure risk of the original .update call.
Hub was refactored and the guides page now lives at /#/guides.
The old /#/dashboard#guides path still redirects, but new
installs should use the correct URL directly.
Backend broadcast payloads for internal_chat.message.deleted and
internal_chat.reaction.deleted used channel_id as the key, but the
frontend ActionCable handlers (and all other internal-chat events)
expect internal_chat_channel_id. This caused deleted messages and
removed reactions to stay visible on screen until a manual refresh.
Rename the key on the backend so the payloads match the convention
shared with message.created/updated and reaction.created, and drop the
defensive fallback on the frontend reaction-deleted handler.
* feat(whatsapp): allow converting inbox between WhatsApp providers
Adds a Convert flow to switch a WhatsApp inbox between the four
supported providers (default/360dialog, whatsapp_cloud, baileys, zapi)
without losing conversations, agents, or history.
- Channel::Whatsapp#convert_provider! runs inside a transaction:
disconnects the old provider, clears provider_connection and
message_templates, assigns the new provider/config, and triggers
webhook setup plus template resync on the new service.
- New POST /api/v1/accounts/:id/inboxes/:id/convert_provider endpoint
guarded by InboxPolicy#convert_provider? (admin only).
- UI adds a Convert button on the inbox Settings page with a
type-to-confirm ConvertInboxModal that lists the effects before
redirecting to a dedicated route reusing the WhatsApp provider
wizard in convert mode (phone number locked, current provider
hidden from the picker).
* chore(whatsapp): polish convert UI colors and expand specs
- Settings: use slate for the Convert trigger and ruby for the modal
confirm to mirror the delete gate instead of the less conventional
amber variant.
- Drop the redundant "current provider is hidden from the list"
sentence from the convert wizard description.
- Add specs for the post-conversion webhook setup path (triggered and
skipped branches) and the sync_templates error-rescue behaviour.
* fix: address CodeRabbit review on convert-provider flow
- Whitelist provider_config keys in the convert endpoint via permit
rather than permit!, and default to an empty hash when omitted so
the request no longer crashes.
- Pre-validate the new provider config before disconnecting the old
session so a bad target config no longer terminates the existing
provider; also keep the disconnect bound to the old provider_url.
- Guard ConvertInboxModal's submit handler so pressing Enter cannot
bypass the type-to-confirm gate, and migrate it to <script setup>.
- Reject invalid ?provider= query values in convert mode so hidden
providers (Twilio, the current provider) cannot be reached via URL.
- Await the inbox fetch in InboxConvert before running the route guard
so directly opening the route for a non-WhatsApp inbox redirects.
- Remove the unreachable second CloudWhatsapp branch in Whatsapp.vue.
* fix: address second CodeRabbit round on convert-provider flow
- Unify provider picker validation so create mode also rejects
unknown ?provider= values, with a single helper that accepts
available providers plus the whatsapp_manual fallback.
- Simplify the pre-validation rollback in convert_provider!: the
errors snapshot/merge dance was redundant because assign_attributes
does not clear errors.
- Follow the repo convention of asserting on error.class.name so the
rollback spec stays stable under reloading/parallel environments.
- Strengthen the controller success spec with provider_connection and
message_templates cleanup invariants, and set Content-Type on the
templates stub so HTTParty parses the empty data array correctly.
* fix: address third CodeRabbit round on convert-provider flow
- Add 360Dialog entry to the Whatsapp provider catalog, keep it hidden
from the create picker (preserving the existing fork behavior) but
expose it in the convert picker where it is a valid target. Restore
URL reachability for ?provider=360dialog in create mode.
- Scope the WHATSAPP_MANUAL allowance to create mode only: the manual
fallback flow is not reachable in convert mode.
- Redirect to the inboxes list in InboxConvert when the inbox is still
absent after the store fetch, so the page no longer stays blank.
- Use an explicit allowlist of WhatsApp providers to gate the Convert
button instead of negating Twilio, so adding a new WhatsApp channel
type will not silently expose the flow.
- Bind the disabled provider display field with :value instead of
v-model, since the underlying computed is getter-only.
- Add Content-Type: application/json to the templates stub in the
model spec so HTTParty parses the empty data array.
* fix: address fourth CodeRabbit round on convert-provider flow
- Reject no-op conversions that target the same provider as the one
already configured, so the endpoint no longer wipes provider
connection and message templates on a request that changes nothing.
- Call the provider service's disconnect directly so failures abort
the conversion instead of being silently swallowed; otherwise the
old external session could remain live while the inbox flips to
the new provider.
- Cover both behaviors with specs.
* fix: address fifth CodeRabbit round on convert-provider flow
- Reset the Vuelidate state when closing ConvertInboxModal so reopening
the gate does not surface stale validation errors.
- Call teardown_webhooks before converting away from whatsapp_cloud so
the Meta webhook subscription is removed for embedded_signup channels,
mirroring the destroy-time cleanup (manual-setup channels keep the
existing no-op behavior). Swallow teardown failures so a flaky Meta
call does not abort the swap.
- Switch the rollback specs to compare message_templates counts instead
of the boolean be_present matcher so they remain meaningful if the
fixture happens to have an empty templates list.
* fix: address sixth CodeRabbit round on convert-provider flow
- Derive the convert header's current-provider label from the shared
PROVIDER_CATALOG so the picker and header stay in sync.
- Assert the full Cloud provider_config payload and the absence of the
Baileys-only provider_url key on both the controller success spec
and the model atomic-swap spec.
- In the sync-error spec, reload and assert that the record was
actually flipped to the new provider before the sync rescue fires,
so the test can't pass on a pre-save failure.
* test: pin 422 error payload on convert_provider negative paths
The unsupported-conversion and invalid-config specs only checked the
status code, so they would have stayed green if the 422 started coming
from a different branch. Pin the response body so each example actually
covers the failure case it names.
* fix(baileys): save custom host as provider_url, not url
The Baileys form was writing the custom endpoint to
provider_config['url'] while the backend reads
provider_config['provider_url']. That silently broke the custom-host
feature for newly created or converted Baileys inboxes: they always
fell back to BAILEYS_PROVIDER_DEFAULT_URL. Align the key on both ends.
* fix(whatsapp): skip second validation pass in convert_provider!
The transaction's save! was re-running validate_provider_config after
the old provider's session had already been disconnected, so a transient
Graph API failure on the second check could roll back the swap while
leaving the external session terminated — the exact inconsistency the
pre-flight valid? was meant to rule out.
Capture the validated provider_config snapshot after valid? (so fields
populated by before_validation callbacks like webhook_verify_token are
preserved) and switch the final persist to save!(validate: false) so the
earlier check stays authoritative.
* fix: normalize provider-conversion failures and pass accountId
- The convert_provider action only rescued ActiveRecord::RecordInvalid,
so disconnect/teardown failures bubbled up as 500 with no stable
payload. Catch StandardError, log the class + message, and return a
422 with a generic user-facing message so the dashboard can surface
the error consistently.
- Nested settings routes live under /accounts/:accountId, so the
router push from Settings.vue must include accountId alongside
inboxId. Mirrors how sibling pages navigate to settings_inbox_show.
* fix: report missing :provider as 400 and sync modal v-model
- The generic rescue StandardError on convert_provider was masking
ActionController::ParameterMissing behind a misleading
provider-conversion error message. Catch it explicitly before the
generic rescue and return 400 with the parameter-missing message.
- ConvertInboxModal's closeModal now drives localShow to false so
parents using v-model:show stay in sync on every close path,
not only when the explicit onClose listener flips the flag.
* fix(whatsapp): serialize concurrent convert_provider calls with_lock
Without a per-record lock, two admin requests against the same inbox
could both pass the pre-flight validation, race the disconnect/save,
and then run setup_webhooks/sync_templates in arbitrary order, leaving
the persisted provider out of sync with the external configuration.
Wrap the whole convert flow in with_lock so the loser blocks until the
winner commits; the subsequent no-op guard then rejects a second
conversion request targeting the provider the first one just set.
* test: harden convert_provider policy + controller failure specs
- Pass accountId explicitly in InboxConvert redirects so the route
navigation mirrors how Settings.vue reaches settings_inbox_convert.
- Add a spec that assigns the agent to the inbox and still expects 401,
so a future regression in InboxPolicy#convert_provider? can no longer
slip past on the show policy alone.
- Add a spec that stubs convert_provider! to raise StandardError and
asserts the controller's generic-failure 422 payload, pinning the
dashboard contract for provider-side failures.
* test: pin convert_provider success response payload
Parse the rendered body and assert provider + provider_config so the
spec catches regressions where the DB is updated correctly but the
serialized response drifts (dashboard store commits response.data).
* fix(whatsapp): reset teardown guard after pre-conversion webhook cleanup
teardown_webhooks memoizes @webhook_teardown_initiated = true to prevent
double execution during destroy. Calling it from convert_provider!
leaves that flag set, so a subsequent destroy! or follow-up conversion
on the same instance would skip webhook removal silently. Reset the
flag in an ensure block so the destroy-time guard stays scoped to
destroy only.
* fix: include accountId in post-conversion redirect params
* test: pin same-provider convert returns 422
* fix(whatsapp): reset template columns when post-conversion sync fails
* fix(convert): enforce provider allowlist in InboxConvert route guard
* test: broaden Cloud templates stub to match account-scoped path
* test(whatsapp): cover cloud to baileys conversion branch
Upstream migration 20260324102005_repurpose_response_bot_flag_for_custom_tools
calls InstallationConfig.value before the normalization migration
(20260418020000) runs. On instances whose serialized_value column holds
native jsonb hashes, that read raises "TypeError: no implicit conversion of
Hash into String" and aborts the whole migration chain, leaving db:migrate
stuck and production unable to deploy.
Restore SerializedValueCoder.load that tolerates either shape. dump is
updated to always emit YAML strings so new writes converge on the upstream
format; the 20260418020000 migration still normalizes the backlog.
Some rows in installation_configs.serialized_value were written as native
jsonb objects by older code paths, while the YAML coder expects JSON-encoded
YAML strings. Reading the object-shaped rows raised "TypeError: no implicit
conversion of Hash into String" in production after the upstream 4.13.0 merge.
Convert every object-shaped row to the YAML-string shape the coder produces,
so the stock serialize :serialized_value, coder: YAML, ... works for all
rows without needing a custom coder.
Devise emails sent from unauthenticated flows (password reset,
unlock, confirmation re-send) were always rendered in the default
locale because Current.account is nil in that context. Use the
user's first account as a fallback so the email respects the
account locale configured by the user.
CHANNEL_SERVICES held frozen references to service class objects, captured
when SendReplyJob was first autoloaded. In test env (cache_classes=false),
Zeitwerk reloads triggered between specs (notably by request specs)
replaced the constants with new class objects, leaving the hash pointing
to the stale ones. RSpec stubs applied to the current constant were then
bypassed when the job called service_class.new(...) through the stale
reference, causing flaky CI failures in spec/jobs/send_reply_job_spec.rb
when sharded together with V2::ReportBuilder + Captain::Preferences specs.
Storing class names as strings and resolving via constantize per call
fixes this and is the standard Rails autoload-safe pattern.
The Rails schema dumper can't capture CREATE FUNCTION statements, so every
db:schema:dump silently drops the execute <<~SQL block that defines
f_unaccent, breaking schema loading downstream (the functional GIN
indexes reference the missing function).
Mirror the existing schema-load enhance hook with a post-dump task that
re-injects the block between the last enable_extension and the first
create_table. Idempotent: skips when the block is already present.
Also bundle the merge-upstream skill that documents the recurring merge
patterns for this fork.
users.message_signature is nullable, so currentUser.message_signature can
arrive as null for accounts without a signature set. Vue prop defaults
only kick in for undefined, so the null passed through v-model to the
Editor, which called MarkdownIt.parse(null) and threw 'Input data should
be a String', breaking the profile settings page.
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>
* fix(whatsapp): preserve green color on chat list typing indicator
The messagePreviewClass computed includes text-n-slate-11/12, which
overrode text-green-500 in the compiled Tailwind order. Split padding
into a dedicated computed and apply only it on the typing preview.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): clear contact typing indicator when message is received
Dispatch CONVERSATION_TYPING_OFF after a new incoming message is
persisted from baileys messages.upsert, so the dashboard clears the
typing/recording indicator without waiting for a paused/unavailable
presence event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(conversations): dispatch messages.read event when unread messages exist
The throttling introduced in upstream #13355 returned early for the
"has unread" branches, skipping dispatch_messages_read_event. That
meant the MESSAGES_READ event only fired when there were no unread
messages, so ChannelListener never called channel.read_messages on
the baileys provider when an agent actually read a conversation.
Consolidate the unread/throttle guard so the dispatch runs in all
paths where update_last_seen_on_conversation runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.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>
# Pull Request Template
## Description
### Description
This PR fixes an issue where the editor would reset content and move the
cursor while typing. The issue was caused by a dual debounce setup
(400ms + 2500ms) that saved content and then overwrote local state with
stale API responses while the user was still typing.
### What changed
* Editor now uses local state (`localTitle`, `localContent`) as the
source of truth while editing
* Vuex store is only used on initial load or navigation
* Replaced dual debounce with a single 500ms debounce (fewer API calls)
* `UPDATE_ARTICLE` now merges updates instead of replacing the article
* Prevents status changes from wiping unsaved content
* Removed `updateAsync` for a simpler update flow
### How it works
User types
→ local ref updates immediately (editor reads from this)
→ 500ms debounce triggers
→ dispatches `articles/update`
→ API persists the change
→ on success: store merges the response (used by other components)
→ editor remains unaffected (continues using local state)
Fixes
https://linear.app/chatwoot/issue/CW-6727/better-syncing-of-content-the-editor-randomly-updates-the-content
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
1. Open any Help Center article for editing
2. Type continuously for a few seconds — content should not reset or
jump
3. Change article status (publish/archive/draft) while editing — content
should remain intact
4. Test on a slow network (use DevTools throttling) — typing should
remain smooth
## 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: Muhsin Keloth <muhsinkeramam@gmail.com>
AI-generated summaries now respect the account's language setting.
Previously, summaries were always returned in English regardless of the
user's configured language, making section headings like "Customer
Intent" and "Action Items" appear in English even for non-English
accounts.
Previous behavior:
<img width="1336" height="790" alt="image"
src="https://github.com/user-attachments/assets/5df8b78b-1218-438d-9578-a806b5cb94ac"
/>
Current Behavior:
<img width="1253" height="372" alt="image"
src="https://github.com/user-attachments/assets/ae932c97-06da-4baf-9f77-9719bc9162e8"
/>
## What changed
- Added explicit account locale to the AI system prompt in
`Captain::SummaryService`
- Updated the summary prompt template to instruct the model to translate
section headings
## How to test
1. Configure an account with a non-English language (e.g., Portuguese)
2. Open a conversation with messages
3. Use the Copilot "Summarize" feature
4. Verify that section headings ("Customer Intent", "Conversation
Summary", etc.) appear in the account's language
---------
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Rename consolidate_contact to consolidate_presence_contact in
PresenceUpdate to avoid overriding the 3-arg version from
GroupContactMessageHandler when both modules are mixed into
IncomingMessageBaileysService.
Fix CSAT spec side effects where conversation callbacks triggered
ActivityMessageJob unexpectedly during test setup.
Adds a Call model to track voice call state across providers (Twilio,
WhatsApp). This replaces storing call data in
conversation.additional_attributes and provides a foundation for call
analytics multi-call-per-conversation support, and future voice
providers.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
- Use optional chaining on presenceSubscribe call in setActiveChat to
handle missing mock in tests
- Update setup_channel_provider spec stubs to include autoPresenceSubscribe
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(whatsapp): show contact typing and recording indicators via baileys presence
Subscribe to WhatsApp presence updates via the baileys-api provider to
display real-time typing and recording indicators in the dashboard.
- Handle presence.update webhook events (composing, recording, paused,
available) and broadcast via ActionCable
- Add conversation.recording event to ActionCable, webhook, and channel
listeners for parity with typing_on/typing_off
- Show "typing..." / "recording..." in green text on the chat list,
replacing the message preview
- Show "X is typing" / "X is recording audio" in the conversation view
- Add presence_subscribe provider config option (default off) to gate
all subscription calls to the baileys-api
- Subscribe to presence on conversation open and periodically (1 min)
for the top 10 chat list conversations
- Consolidate contact LID from presence.update jidAlt payload
- Prevent echo-back of contact typing events to the channel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Filter chat list typing indicator to contact-only events
- Add dedupe to presence subscribe bulk calls
- Use strong parameters for conversation_ids
- Remove redundant YAML quotes in swagger webhook enum
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Extract phone from data[:id] when JID is @s.whatsapp.net (fallback
when jidAlt is absent)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Filter recording users in getTypingUsersText to show correct names
- Add 10s timeout to presence_subscribe HTTP request
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: scope typing timer per user instead of per conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make presence subscribe best-effort with rescue per channel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Add messagePreviewClass to typing preview for consistent padding
- Fix specs to use WebMock assertions instead of instance spying
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Description
ConversationReplyMailer#parse_email calls
Mail::Address.new(email_string).address without error handling. When an
account's support_email contains a non-email string (e.g., "Smith
Smith"), the mail gem raises Mail::Field::IncompleteParseError, crashing
conversation transcript emails.
This has caused 1,056 errors on Sentry (EXTERNAL-CHATINC-JX) since Feb
25, all from a single account that has a name stored in the
support_email field instead of a valid email address.
Closes
https://linear.app/chatwoot/issue/CW-6687/mailfieldincompleteparseerror-mailaddresslist-can-not-parse-orsmith
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
## 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: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
## Linear ticket
https://linear.app/chatwoot/issue/CW-6834/billing-upgrade-didnt-work
## Description
A `customer.subscription.updated` Stripe webhook for account 76162
returned 200 OK but did not persist the new `subscribed_quantity`. Root
cause: a race condition between the webhook handler and
`increment_response_usage` (Captain usage counter), both doing
read-modify-write on the `custom_attributes` JSONB column. The webhook
wrote `quantity: 6`, then a concurrent `save` from
`increment_response_usage` overwrote the entire hash with stale data —
restoring `quantity: 5`.
Fix: use atomic `jsonb_set` so usage counter updates only touch the
single key they care about, instead of rewriting the whole
`custom_attributes` hash. `increment_custom_attribute` also performs the
increment in SQL, making concurrent increments correct as well.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- New regression spec in `handle_stripe_event_service_spec.rb` that
simulates concurrent webhook + `increment_response_usage` and asserts
both `subscribed_quantity` and `captain_responses_usage` survive
- Existing account, billing, captain, and topup specs all pass 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
- [ ] Any dependent changes have been merged and published in downstream
modules
The f_unaccent SQL function created by the internal chat migration is
not natively captured by schema.rb, causing db:test:prepare to fail
when creating indices that depend on it. Add the function definition
to schema.rb and extend SchemaDumper to preserve it across future dumps.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Render English block first, then Portuguese, with H2 country-flag
headings outside the user-notes markers so GitHub viewers see clearly
separated sections without affecting fazer.ai rendering
- Document the full release body (Changes commit list + both locale
sections) as the canonical example
- Replace the non-existent `gh release edit` with `gh api PATCH` for
backfills
- Drop the unrealistic "cannot write equivalent" fallback clause
- Document bilingual (pt-BR + en) user-notes blocks required in every
GitHub release body, rendered on fazer.ai/chatwoot-release-notes
- Add .claude/skills/release-notes skill so the agent drafts and
validates the blocks before any release create/edit/backfill, with
reference from CLAUDE.md (AGENTS.md)
- Point the admin "new version available" banner and the profile-menu
Changelog link at fazer.ai/chatwoot-release-notes
- Stop tracking .claude/settings.json (per-developer config moves to
settings.local.json); ignore only .claude/**/*.local.* so the
release-notes skill ships with the repo