Captain can now use each assistant's saved setting when a customer stops
replying. Captain can review the conversation and resolve or hand it
off, resolve it after the selected time without review, or leave it
pending until the customer replies.
The job checks the conversation again while holding a database lock
before it changes the status. A new customer reply or another worker
cannot cause an outdated resolve or handoff.
## Closes
[AI-163](https://linear.app/chatwoot/issue/AI-163)
## What changed
- Added assistant modes for review, always resolve, and wait for the
customer.
- Kept the account setting as the fallback for assistants that do not
have a saved mode.
- Skipped scheduling when resolution is disabled on the assistant or
through the older account setting.
- Rechecked the conversation status and activity time before each
resolve or handoff.
- Recorded events only after a status change succeeds.
- Kept out of office messages out of campaign conversations.
## How to test
1. Set an assistant to review conversations. Run the inactivity job with
complete and incomplete decisions. Confirm the first conversation is
resolved and the second is handed off.
2. Set the assistant to always resolve. Confirm an eligible pending
conversation is resolved after the selected time.
3. Set the assistant to wait for the customer. Confirm the scheduler
does not enqueue the inactivity job and the conversation remains
pending.
4. Add a customer reply while the review is running. Confirm the job
does not resolve or hand off the updated conversation.
5. Run two workers for the same conversation. Confirm only one status
change and one event are recorded.
---------
Co-authored-by: iamsivin <iamsivin@gmail.com>
Twilio voice calls now get an AI transcript alongside the recording.
Once a call ends and its recording is stored, we transcribe it and show
the text under the audio player in the call bubble — the same experience
WhatsApp voice notes already have. Transcription runs on Captain and
consumes Captain response credits, so it only kicks in for accounts with
Captain enabled and audio transcriptions turned on.
## How to test
1. On an account with Captain enabled and Settings → Account → Audio
transcriptions on, make a call on a Twilio voice inbox and hang up.
2. Open the conversation. The voice call bubble shows the recording
player once Twilio delivers the recording.
3. Shortly after, the transcript appears under the player — no refresh
needed.
4. Turn audio transcriptions off (or exhaust Captain credits) and
repeat: the recording still appears, the transcript does not.
## What changed
- `Llm::SpeechToTextService` (new) — blob-in/text-out transcription
engine extracted from `Messages::AudioTranscriptionService`: size limit,
temp-file download, model resolution via `Llm::FeatureRouter`, the
OpenAI call, and Captain credit accounting. `.available_for?` holds the
shared gate.
- `Messages::AudioTranscriptionService` — now a thin wrapper over that
engine; its public contract is unchanged, so
`Captain::OpenAiMessageBuilderService` is unaffected.
- `Voice::CallTranscriptionService` / `Voice::CallTranscriptionJob`
(new) — transcribe `call.recording` into `calls.transcript`, then
rebroadcast the message so clients pick it up over the wire.
- `Voice::Provider::Twilio::RecordingAttachmentService` — enqueues the
job after the recording is attached.
The API and frontend needed no changes: `calls.transcript` already
existed, `_call.json.jbuilder` already serialized it, and
`VoiceCall.vue` already fed it to the audio chip. Nothing had ever
written the column.
Also wires `instrument_audio_transcription`, which existed but was never
called, so both transcription paths now emit LLM spans.
WhatsApp click-to-chat ad referrals are already stored on incoming
messages, but agents only see the customer’s text. This displays the
originating ad preview directly in the message bubble, including
available media, headline, body, and source link. Attribution
identifiers remain hidden.
### Things to know
- The card renders only when `content_attributes.referral` is present.
- Invalid or non-HTTP media and source URLs fall back safely.
- Existing messages without referral metadata are unchanged.
### How to test
1. Open a WhatsApp conversation containing an incoming message with
`content_attributes.referral`.
2. Confirm the ad preview appears above the customer text.
3. Confirm the source opens in a new tab and missing or invalid media
does not break the message bubble.
Fixes
https://linear.app/chatwoot/issue/CW-6206/add-whatsapp-ad-preview-support-in-chatwoot-inbox
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
# Pull Request Template
## Description
This PR fixes the mention picker staying open when switching between
Reply and Private Note, or when switching conversations while a picker
is open. The picker could keep a stale range from the previous editor
state, and selecting an agent from it would crash the editor with
`RangeError: Position N out of range`.
The pickers now close whenever the editor content is rebuilt, and the
stored suggestion range is cleared before creating the new editor state.
This is a pre-existing issue and not a regression from the picker PR.
For the mention picker to open, `@` needs to be at the start of a line
or preceded by whitespace, so the repro uses `hello @` instead of
`abc@`.
#### What changed
Rebuilding the editor state creates new plugin instances, so the old
suggestion plugin is removed without calling `onExit`. This leaves the
picker's stored range pointing to a document that no longer exists.
`reloadState` now clears the stored suggestion range and closes all open
pickers before rebuilding the editor state.
This is a pre-existing issue and not a regression from the picker PR.
Fixes
https://linear.app/chatwoot/issue/CW-7919/mention-picker-opens-on-an-empty-private-note-and-crashes-when-an
## Type of change
- [x] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
## How Has This Been Tested?
### Steps to reproduce
1. Open a conversation and stay in Reply mode.
2. Type `hello @` to open the mention picker.
3. Switch to Private Note.
4. Click any agent from the still-open picker.
5. The editor crashes with `RangeError: Position N out of range`.
6. The same issue can be reproduced by switching to another conversation
while a picker is open.
## 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
This PR adds search and preview support to the mention, variable, and
emoji pickers, bringing them in line with the canned response picker.
All editor pickers now use the same component with their own search
field, keyboard navigation, caret-anchored placement, and a preview
pane.
Variables show their description and the value resolved for the current
conversation, emojis show the glyph, name, shortcode, and category, and
mentions show relevant agent or team details such as availability, role,
auto-assign, and membership.
This also fixes company custom attributes incorrectly appearing as
contact custom attributes in the variable picker. They could create
duplicate entries and insert variables that always resolved to an empty
value, so they are now excluded.
The old `MentionBox` component has also been removed since the variable
and emoji pickers were its only remaining users.
Fixes
https://linear.app/chatwoot/issue/CW-7854/inconvenient-canned-response-picker-and-lack-of-personal-canned
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Screenshots
**Emoji Picker**
<img width="1302" height="527" alt="image"
src="https://github.com/user-attachments/assets/a491b149-d547-4ab4-b990-a054faf38ac1"
/>
**Variables Picker**
<img width="1305" height="487" alt="image"
src="https://github.com/user-attachments/assets/42a6d334-4f70-47e2-ada4-531cf8619454"
/>
<img width="444" height="556" alt="image"
src="https://github.com/user-attachments/assets/bccd15e1-e5af-4362-b1b5-27877910670c"
/>
**Tag agents/teams Picker**
<img width="1308" height="502" alt="image"
src="https://github.com/user-attachments/assets/547d9142-a6e5-4606-a9b7-01e824868b29"
/>
<img width="1308" height="502" alt="image"
src="https://github.com/user-attachments/assets/28cd4919-5875-462a-adc8-90c0fd8e8a3f"
/>
<img width="444" height="556" alt="image"
src="https://github.com/user-attachments/assets/33eaa131-c033-49e4-aa93-343c2a976d5b"
/>
### Steps
1. Type `@` in a private note. Verify agents and teams appear under
separate headings, and the preview shows availability and role for an
agent, and auto-assign and membership status for a team.
2. Type `{{` in a reply. Verify the preview shows the variable
description and the value resolved for the current conversation. Also
verify that a variable with no value clearly indicates that no value is
available.
3. Verify company custom attributes no longer appear in the variable
picker.
4. Type `:` followed by at least two characters. Verify emojis can be
searched by name and shortcode, and the preview shows the glyph, name,
and category.
5. For each picker, navigate with the arrow keys or Tab, insert with
Enter or a click, and close with Escape.
6. Reopen a conversation with a draft containing `@name`, `/shortcode`,
or `{{contact`. Verify the corresponding picker opens with the existing
text populated in its search field.
## 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
- [x] 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
MFA-authenticated users now remain signed in across browser restarts,
matching the session lifetime of password-authenticated users.
## Closes
-
[CW-7898](https://linear.app/chatwoot/issue/CW-7898/mfa-users-are-logged-out-when-the-browser-session-ends)
## How to reproduce
1. Enable MFA for a user.
2. Sign in and complete OTP or backup-code verification.
3. End the browser session, then reopen the browser and return to
Chatwoot.
4. Before this change, the user is sent back to the login page.
## Root cause and evidence
Before this change, successful MFA verification wrote
`cw_d_session_info` directly through `document.cookie` without an
`Expires` or `Max-Age` attribute. This makes it a session cookie:
- [MFA cookie creation on the base
commit](a4eae9710a/app/javascript/dashboard/components/auth/MfaVerification.vue (L70-L87))
- [MDN session-cookie
behavior](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie)
- [Chromium cookie persistence
documentation](https://chromium.googlesource.com/chromium/src/+/master/net/cookies/README.md)
Password login already uses `setAuthCredentials`, which derives a
persistent cookie expiry from the authentication response:
- [Existing credential
helper](a4eae9710a/app/javascript/dashboard/store/utils/api.js (L28-L36))
- [Configured two-month token
lifetime](a4eae9710a/config/initializers/devise_token_auth.rb (L8-L10))
This change routes successful MFA verification through the same
credential helper and existing verified event. The regression test
asserts that MFA credentials use this shared persistence path.
Session-cookie restoration is browser and profile dependent. Chromium
may persist session cookies when restoring a previous browsing session,
so the problem can be masked on some restarts. This PR does not rely on
a desktop-versus-mobile distinction.
## How to verify
1. On `develop`, complete an MFA login.
2. Inspect `cw_d_session_info` in browser developer tools. Its expiry is
shown as `Session`.
3. Repeat on this branch.
4. Confirm that `cw_d_session_info` has a concrete expiry derived from
the authentication response instead of `Session`.
Automations that send an email transcript now let you insert the
conversation contact's email directly into the recipient field, instead
of only accepting a hardcoded address typed in by hand.
## Closes
https://linear.app/chatwoot/issue/CW-7535/allow-automation-to-send-conversation-transcripts-to-user-emails
## How to test
1. Go to **Settings → Automation → Create Automation** (or edit an
existing rule).
2. Add the **Send an Email Transcript** action.
3. Click **Use contact's email** next to the recipient field — it
inserts `{{contact.email}}` into the field, appending to any address
already typed rather than replacing it.
4. Save the rule and trigger it on a conversation with a contact that
has an email address; confirm the transcript is delivered to that
contact.
## What changed
- `AutomationActionInput.vue`: added a "Use contact's email"
quick-insert action next to the recipient field for the
`send_email_transcript` action, and switched the input from
`type="email"` to `type="text"` so the dynamic token isn't fought by
native email-format validation.
- No backend changes were needed — `send_email_transcript` already
resolves `{{contact.email}}` via the existing Liquid templating support
in `ActionService#send_email_transcript`; this change only exposes that
capability in the UI.
- This also benefits the Macros editor, which shares the same input
component.
---------
Co-authored-by: iamsivin <iamsivin@gmail.com>
Facebook inbox creation failures in Settings could leave users on the
same screen without explaining what went wrong. This change preserves
the backend error response and displays it through the existing toast
mechanism, making account-limit and other creation failures visible and
actionable.
The shared action now passes the original API error through, so the
newer onboarding flow also receives the specific backend message.
Successful Facebook inbox creation remains unchanged.
### How to reproduce
1. Use an account that has reached its inbox limit.
2. Go to Settings → Inboxes → Add Inbox → Facebook.
3. Select a Facebook Page and create the inbox.
4. Previously, the loading state ended without any visible error.
### How to test
1. Attempt Facebook inbox creation while the backend returns an HTTP 402
account-limit error.
2. Confirm the returned error appears as a toast message.
3. Retry after increasing the account limit and confirm inbox creation
continues normally.
### Things to know
The toast falls back to a generic localized creation error when the API
response does not contain a displayable message.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
# Pull Request Template
## Description
This PR reworks the canned response picker to make it easier to search,
browse, and preview canned responses before inserting them. Typing `/`
now opens a larger picker with its own search field and a preview pane.
Previously, only a few responses were visible at a time, there was no
way to preview the full content, and searching relied on typing into the
composer, which stopped working for multi-word queries.
Search is now handled entirely inside the picker, so the composer stays
untouched while searching. Results match both the canned response
shortcut and its content, and each result shows a snippet centered
around the matched text instead of always displaying the beginning of
the response. The preview renders the response exactly as it will be
inserted, with variables resolved against the current conversation and
formatting unsupported by the channel already stripped.
The picker is positioned relative to the current typing line and
teleported to `body`, so it is no longer clipped by the composer. It
behaves consistently across the reply editor, the New Conversation
composer, and narrower editors such as Contact Notes, where the preview
moves below the list instead of disappearing.
This also fixes a pre-existing bug where variables without a value were
removed from the inserted text instead of being left for the backend to
resolve. In the New Conversation composer, where no variables are
available, all `{{ }}` placeholders were previously being silently
removed.
Fixes
https://linear.app/chatwoot/issue/CW-7854/inconvenient-canned-response-picker-and-lack-of-personal-canned
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Screenshots
<img width="1135" height="576" alt="image"
src="https://github.com/user-attachments/assets/b5f27b94-eeb5-4ac6-b6d9-da7dfd8c2306"
/>
<img width="393" height="490" alt="image"
src="https://github.com/user-attachments/assets/779f88b1-4958-41f5-9f51-c2eb8db7e53c"
/>
### Steps
1. Open a conversation and type `/` in the reply editor.
2. Search using a multi-word phrase that appears within a canned
response. Verify the matching response appears with a snippet centered
around the matched text.
3. Navigate the results with the arrow keys or Tab and verify the
preview updates.
4. Press Enter or click a response to insert it, and press Escape to
close the picker.
5. Repeat in a narrow editor such as Contact Notes and verify the
preview pane moves below the list.
6. In the New Conversation composer, insert a canned response containing
variables and verify the `{{ }}` placeholders are kept rather than
removed.
## 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
Captain V2 assistants can now persist a configurable inactivity timer
and choose whether inactivity resolution sends the saved closing message
or resolves silently. This PR contains only the API, persistence,
runtime behavior, and backend specs.
## Closes
[AI-163](https://linear.app/chatwoot/issue/AI-163)
## Depends on
Stack 2 of 5. Based on the assistant-policy foundation in #15299. The
frontend follows in #15308.
## What changed
- Added per-assistant inactivity duration and resolution-message
settings with safe defaults.
- Restricted the Part 2 settings API to Captain V2 while keeping the
Part 1 policy mode available without V2.
- Updated inactivity handling to use the assistant timer and skip the
public resolution message when disabled.
- Serialized the effective timer and message settings for the frontend.
- Added model, request, and job coverage, including the explicit Captain
V2 boundary.
## How to test
1. Enable Captain V2 and update `auto_resolve_after` and
`send_inactivity_resolution_message` through the assistant API.
2. Run the inactivity job and confirm it uses the assistant timer.
3. Disable the resolution message and confirm the conversation resolves
silently.
4. Disable Captain V2 and confirm timer/message updates are ignored
while `auto_resolve_mode` remains updateable.
---------
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Improves the WhatsApp template listing so account admins can understand
template state and refresh templates from one place. Templates that have
not entered the WhatsApp approval flow now use clearer wording, the
latest sync attempt appears once at page level, and templates can be
filtered by content type.
The page can now start a template sync across all active WhatsApp
inboxes in the account and reports complete or partial failures. The New
template action is removed for now; provider-side template management
remains unchanged.
### Related
Related: https://github.com/chatwoot/chatwoot/pull/15312
### How to test
1. Open **Settings → Templates** in an account with WhatsApp templates.
2. Confirm the latest sync attempt appears below the page description
and is no longer repeated on every template.
3. Confirm an unsubmitted template displays **Not submitted for WhatsApp
approval**.
4. Filter templates by content type and verify the list updates.
5. Click **Sync templates** and confirm sync starts for the account's
active WhatsApp inboxes.
6. Confirm the **New template** button is not displayed.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
AgentBot-owned conversations are now treated as assigned across
conversation lists, counts, pagination, permissions, unread membership,
human auto-assignment, and advanced assignee filters.
## Closes
-
https://linear.app/chatwoot/issue/CW-7689/align-agent-bot-ownership-with-unassigned-counts-and-pagination
## Follow-ups
-
https://linear.app/chatwoot/issue/CW-7870/refresh-agentbot-ownership-state-when-deleting-an-agent-bot
tracks ownership refresh during AgentBot deletion.
-
https://linear.app/chatwoot/issue/CW-7899/refresh-saved-filter-totals-after-live-conversation-ownership-changes
tracks the existing saved-filter header count refresh gap.
## Why
The backend treated every conversation without a human assignee as
unassigned, even when an AgentBot owned it. The frontend already hid
AgentBot-owned conversations from the Unassigned list, so counts,
pagination, filters, unread membership, direct-access permissions, and
auto-assignment could disagree with the visible queue.
## What changed
- Treat conversations with either a human assignee or AgentBot owner as
assigned.
- Keep conversation counts, pagination, unread memberships, advanced
filters, and automation assignee conditions aligned with the shared
ownership semantics.
- Keep human assignee equality and not-equality filters human-only, even
when a human and AgentBot have the same numeric ID.
- Exclude AgentBot-owned conversations from both legacy and V2 human
auto-assignment, and from unassigned-only Enterprise access.
- Preserve AgentBot ownership when Twilio or WhatsApp call flows reuse
or accept an assigned conversation.
- Emit ownership-change updates when only the AgentBot owner changes, so
connected clients refresh queue state.
## Validation
- AgentBot assignment changed ownership to the bot, moved the
conversation to pending, and removed it from the open queue.
- Assignee "is present" returned human- and AgentBot-owned
conversations; "is not present" returned only genuinely unassigned
conversations.
- Automation assignee presence conditions treated AgentBot ownership as
present and did not execute the absent-owner path.
- Live human-assignee equality and not-equality filters excluded
AgentBot-owned conversations, including numeric ID collisions.
- A 32-conversation pending queue loaded across pagination with matching
totals and no missing or duplicate rows.
- AgentBot ownership changes and human takeover updated filtered rows
immediately without a reload.
- Human takeover opened the conversation and restored the public reply
composer; subsequent unassignment kept the conversation open.
- An unassigned-only custom-role agent saw only genuinely unassigned
conversations and could not see AgentBot-owned conversations.
- AgentBot-owned conversations showed the handled-by-bot banner, Take
over action, and disabled public reply composer.
- New conversations in the connected inbox were assigned to the AgentBot
and excluded from human auto-assignment.
- Opening an AgentBot-owned conversation did not let the legacy
assignment callback or its locked recheck replace the bot.
- Moving an AgentBot-owned conversation to an auto-assigning team
preserved the bot and did not create a second human owner.
- Twilio conference pickup, Twilio outbound reuse, WhatsApp outbound
reuse, and inbound WhatsApp acceptance preserved existing AgentBot
owners.
- Focused ownership, filters, pagination, permissions, unread-count,
auto-assignment, frontend, and lint checks passed locally.
- GitHub Actions, Docker builds, CircleCI, security checks, and the
final Codex review are green on the final head.
Captain assistants now support **audience** and **schedule** controls,
so you can decide *who* an assistant replies to and *when* it's on duty.
By default nothing changes, an assistant still responds to every
conversation in its connected inboxes but you can now narrow that down.
- **Audience**: build a condition tree (contact attributes, conversation
attributes, and custom attributes) with and/or groups, mirroring the
contact-segment filter semantics. Only conversations whose contact
matches the audience get a Captain reply.
- **Schedule**: choose when Captain replies — *Anytime*, *During
business hours*, or *Outside business hours* (based on each inbox's
configured working hours; inboxes without business hours are always
covered).
When an assistant opts out of a conversation (contact outside the
audience, or off-schedule), the conversation is routed to the human
queue instead of being parked pending on a silent bot — both on initial
creation and on reopen.
Fixes
https://linear.app/chatwoot/issue/CW-7414/audience-and-availability-controls
|Audience|Availability|
|--|--|
| <img width="1132" height="627" alt="Screenshot 2026-06-30 at 5 52
09 PM"
src="https://github.com/user-attachments/assets/866910e0-e1d7-4248-8630-d91afc758688"
/> | <img width="1131" height="539" alt="Screenshot 2026-06-30 at 5 52
13 PM"
src="https://github.com/user-attachments/assets/aad0d6f7-ceb7-4546-a049-095c5b46b483"
/> |
## How to test
1. Open **Captain → Assistants → (an assistant) → Settings**.
2. Under **Audience**, add a condition or condition group (e.g. `Contact
language equal_to en`) and save. Start a conversation from a contact
that does *not* match — Captain should stay silent and the conversation
should land in the human (open) queue instead of pending.
3. With a matching contact, Captain should respond as before.
4. Under **Schedule**, pick **During business hours** (or **Outside
business hours**) on an inbox that has working hours configured, and
confirm Captain only engages within/outside that window. An
empty/`Anytime` schedule always responds.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
## Linear Ticket
-
https://linear.app/chatwoot/issue/CW-6884/nudge-users-with-a-dashboard-banner-when-2fa-backup-codes-run-low
## Description
Shows a dashboard-wide banner when the signed-in user has 3 or fewer
unused backup codes left (amber), turning to an alert style at 0
remaining. Clicking "Generate codes" takes the user to the MFA settings
page so they can regenerate codes before they get locked out. Inspired
by Google's post-backup-code-use nudges.
## How to test
1. Sign in as a user with MFA enabled.
<img width="1512" height="824" alt="Screenshot 2026-08-05 at 4 52 15 PM"
src="https://github.com/user-attachments/assets/08138f3e-cc15-451e-bbcc-7772dc2a875c"
/>
<img width="1507" height="701" alt="Screenshot 2026-08-05 at 4 54 02 PM"
src="https://github.com/user-attachments/assets/580051fa-cbb5-47c1-81a3-258b7c5b5a03"
/>
2. In a Rails console, simulate a low state by marking most backup codes
as used:
```ruby
u = User.find_by(email: '<your user>')
codes = u.otp_backup_codes.dup
(0...8).each { |i| codes[i] = 'XXXXXXXX' }
u.otp_backup_codes = codes
u.save!
```
3. Reload any dashboard page — the amber banner should appear with a
"Generate codes" CTA.
4. Click the CTA — it should route to **Profile → Two-Factor
Authentication**, where you can regenerate codes.
5. Set the count to 0 (mark all 10 as `'XXXXXXXX'`) — banner should
switch to the red/alert style.
6. Regenerate codes — banner should disappear on the next dashboard page
load.
## 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
> Base is the [disable-with-backup-code PR
branch](https://github.com/chatwoot/chatwoot/pull/14102) so CTAs around
recovery are consistent; rebase onto `develop` once that merges.
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
When an agent has multiple tabs or windows open on the same account, an
inbound WhatsApp call rings in all of them, as expected. But once the
call is answered in one tab, the others never found out — they kept
showing the incoming-call popup and playing the ringtone indefinitely,
as if the call were still waiting to be picked up.
## How to reproduce
1. Log into the same agent account in two browser tabs.
2. Receive an inbound WhatsApp call (rings in both tabs).
3. Accept the call in one tab.
4. The other tab keeps ringing and shows the call as still incoming.
## What changed
- The backend already broadcasts a `voice_call.accepted` event
account-wide when a call is answered, but the dashboard never had a
listener registered for it — the event was silently dropped. Added the
missing handler in `actionCable.js` so every tab except the one that
owns the now-active call clears its ringing state.
- Added `root: true` to `.eslintrc.js` so ESLint config resolution stops
at the project root instead of also picking up a parent directory's
config (this repo's git-worktree layout nests worktrees under the main
checkout, which was causing an ambiguous plugin-resolution error for
anyone linting from a worktree).
---------
Co-authored-by: Sony Mathew <sony@chatwoot.com>
When an agent starts a new conversation, the WhatsApp template picker in
the composer only shows templates that were already loaded into the
frontend store — it doesn't refresh when the composer opens. If a
template sync completed after the store was last populated, the newly
synced template shows up on the inbox's Settings > Templates page (which
always refetches on load) but not in the New Conversation composer,
since that view relied solely on the account-cache-invalidated websocket
event, which doesn't always reach an already-open session in time.
## What changed
- `ComposeConversation.vue` now dispatches a cache-aware `inboxes/get`
refetch every time the composer popover opens, so the WhatsApp template
list is current before an agent picks a template to message a customer.
The refetch checks the account's cache key first and only re-pulls the
full inbox list when it's actually stale, so it stays cheap in the
common case.
## How to reproduce
1. Sync/update WhatsApp templates for an inbox (e.g. via Settings >
Inboxes > [WhatsApp inbox] > Sync Templates).
2. Without reloading the page, open the New Conversation composer for
that inbox and check the WhatsApp template picker — a newly synced
template may be missing until this fix.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
The unsaved changes panel in the Help Center article editor now uses the
shared `SidePanel` component instead of its own custom drawer. It now
matches the rest of the dashboard, with the same slide-in animation,
backdrop, and close button for a consistent experience.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Screenshots
**Before**
<img width="1530" height="879" alt="image"
src="https://github.com/user-attachments/assets/4ef7eafb-0cd6-455f-a970-e1411a24ef25"
/>
**After**
<img width="1530" height="879" alt="image"
src="https://github.com/user-attachments/assets/be99ee0d-1906-4a49-967b-3ba1f4fa40b6"
/>
## 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
This PR fixed the conversation list was incorrectly rendering the avatar
upload overlay on every conversation card, even though uploads aren't
supported there. Clicking it could throw a `TypeError: Cannot read
properties of null (reading 'click')`. Conversation cards now only show
the selection checkbox, while avatar uploads continue to work everywhere
they're supported.
### Cause
`ConversationCard` always passed the `#overlay` slot, but the checkbox
inside it was wrapped in `v-if`. When the checkbox wasn't rendered, Vue
treated the slot as empty and fell back to the default upload overlay
from `Avatar`.
That overlay's click handler expects a file input, but the file input is
only rendered when `allowUpload` is enabled. Since conversation cards
never enable uploads, clicking the overlay could dereference a null file
input and throw.
### How to reproduce
This isn't reliably reproducible manually. It only happens when the
upload overlay becomes visible while the card's internal hover state is
out of sync with the browser's CSS `:hover` state. In normal
interaction, entering the card immediately updates the hover state and
shows the checkbox instead, so the issue effectively self-recovers.
The new test reproduces this state directly and verifies the fix.
## What changed
* Moved `v-if="allowUpload"` from the hidden file input to the upload
overlay itself, so the overlay and file input are always mounted
together.
* Added `Avatar.spec.js` coverage for the overlay slot, including the
empty-slot case that triggered this bug, along with the existing upload,
delete, badge, sizing, initials, and image fallback behavior.
Fixes
https://linear.app/chatwoot/issue/CW-7726/typeerror-cannot-read-properties-of-null-reading-clickhttps://chatwoot-p3.sentry.io/issues/7291677410/?project=4507182691975168&referrer=Linear
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshots
**Before**
<img width="1683" height="847" alt="image"
src="https://github.com/user-attachments/assets/1b746be4-0425-4a68-953b-193e70411032"
/>
**After**
<img width="1683" height="847" alt="image"
src="https://github.com/user-attachments/assets/e0c78aba-6053-442c-905a-373c8d2be123"
/>
## 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
Cloud accounts without access to WhatsApp Quick setup currently fall
directly into manual configuration, with no explanation of how to
request the easier Meta flow.
This adds a compact access-request card above manual setup. It explains
that Quick setup with Meta can connect either a new number or an
eligible existing number from the WhatsApp Business app, then opens the
existing support widget so the account can be reviewed. The enabled
state now uses the same customer-facing language.
Related: https://github.com/chatwoot/chatwoot/pull/15318
<img width="1428" height="1110" alt="CleanShot 2026-08-05 at 14 12
31@2x"
src="https://github.com/user-attachments/assets/914e5fd7-eb5a-438c-9706-0ecd8e004fbf"
/>
### Things to know
- The access card is Cloud-only and appears when the account-level
WhatsApp Quick setup feature is disabled.
- The global Meta incident restriction still takes precedence and
continues to show the existing incident warning.
- Manual setup remains available, and self-hosted behavior is unchanged.
- Requesting access opens Chatwoot support; it does not automatically
enable the account feature.
- Meta documents the existing WhatsApp Business app number path as
coexistence onboarding:
https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users
### How to test
1. On Chatwoot Cloud, set `DISABLE_META_INBOX_CREATION` to `false`.
2. Open WhatsApp inbox creation for an account without
`whatsapp_embedded_signup_inbox_creation`.
3. Confirm the Quick setup with Meta request card appears above the
unchanged manual configuration form.
4. Select **Request access** and confirm the support widget opens.
5. Enable `whatsapp_embedded_signup_inbox_creation` for the account and
reload.
6. Confirm the enabled Quick setup with Meta screen appears and
describes both new numbers and eligible existing WhatsApp Business app
numbers.
7. Set `DISABLE_META_INBOX_CREATION` to `true` and confirm the incident
warning appears instead of the access-request card.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
WhatsApp Business app Coexistence inboxes currently see the manual
migration prompt even though they cannot move to the generic manual
setup flow without losing the supported Coexistence path.
This change waits for WhatsApp health data and shows manual transfer
only when Meta confirms the number is not connected to the WhatsApp
Business app. Other eligible Embedded Signup inboxes continue to see the
migration prompt.
### Things to know
- This is frontend-only gating; the migration API behavior is unchanged.
- The prompt remains hidden while the WhatsApp Business app state is
unknown or unavailable.
### How to test
1. Enable `whatsapp_manual_transfer` for an account with an Embedded
Signup WhatsApp inbox.
2. Open an inbox whose health response reports `is_on_biz_app: true`;
confirm the migration banner and dialog are unavailable.
3. Open an inbox whose health response reports `is_on_biz_app: false`;
confirm the migration banner appears and opens the existing manual
migration dialog.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
WhatsApp Flow submissions currently reach Chatwoot as
`interactive.nfm_reply` messages, but their structured answers are
discarded. Agents see an empty message and outbound webhooks cannot
access the submitted data.
This change preserves the Flow response in message content attributes
and renders a readable response card in the conversation. Existing
WhatsApp messages and other interactive message types remain unchanged.
Fixes https://github.com/chatwoot/chatwoot/issues/13970
<img width="1234" height="1350" alt="CleanShot 2026-08-04 at 00 40
14@2x"
src="https://github.com/user-attachments/assets/884b3a5c-bab3-4196-8037-776e6c41ab2a"
/>
### How to reproduce
1. Send an approved WhatsApp template containing a Flow button.
2. Complete and submit the Flow from WhatsApp.
3. Open the conversation in Chatwoot.
4. Observe that the incoming message has no visible content and the
submitted fields are absent from the message webhook.
### What changed
- Parse `interactive.nfm_reply.response_json` for incoming WhatsApp
Cloud messages.
- Store the Flow name, body, and structured response under
`content_attributes.whatsapp_flow_response`.
- Add a dedicated conversation bubble that formats submitted field names
and values.
- Preserve original response keys while converting message attributes to
the frontend shape.
- Keep `flow_token` available in stored/webhook data while omitting it
from the agent-facing card.
- Add focused backend, helper, and component regression coverage.
### How to test
1. Send a WhatsApp Flow template to a contact.
2. Submit the Flow with multiple field types, including free text and a
selection.
3. Confirm the incoming conversation message displays each submitted
field and value.
4. Confirm an empty-answer Flow still renders a visible “Submitted a
flow response” message.
5. Inspect the `message_created` webhook and confirm the structured
response is present in
`content_attributes.whatsapp_flow_response.response_json`.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
## Description
Adds Freshdesk as an integration import source so administrators can
validate a Freshdesk domain and API key, then import contacts, tickets,
public replies, customer replies, and private notes while tracking
progress from Data Imports.
The integration has now been validated against a live Freshdesk trial
tenant with contacts, Web Chat and phone tickets, public replies,
customer replies, a private note, pagination, requester expansion, and
attachment metadata. That validation found and fixed the current Web
Chat source mapping and prevented the ticket description from
duplicating the initial Web Chat message.
Related: #15116
## Closes
Closes
[CW-7639](https://linear.app/chatwoot/issue/CW-7639/freshdesk-freshworks-migration)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## What changed
- Added a shared source adapter, importer, job, retry, restart,
creation, and placeholder inbox contract used by Intercom and Freshdesk.
- Added Freshdesk API authentication, contact and ticket pagination,
requester expansion, conversation retrieval, normalization, channel
grouping, and error handling.
- Added current Freshdesk source identifiers through SMS, including Web
Chat source `15`, and grouped equivalent sources into placeholder
inboxes.
- Used Web Chat conversation events as the complete message history so
the generated ticket description does not duplicate the initial customer
message.
- Preserved Freshdesk ticket subjects in source metadata and added a
sanitized live-derived Web Chat fixture with structured bodies and
attachment metadata.
- Added Freshdesk selection, domain and API key validation, and
provider-neutral import status handling in the Data Imports UI.
## How to test
1. Enable the data_import feature for an account and open Settings >
Data > New import.
2. Select Freshdesk and enter a Freshdesk domain and API key.
3. Select contacts and/or conversations, validate the credentials, and
start the import.
4. Confirm progress is displayed and imported tickets appear as resolved
conversations in Freshdesk placeholder inboxes with public replies and
private notes preserved.
5. Verify Web Chat tickets appear in the Chat placeholder inbox and the
initial customer message is imported once.
6. Verify an abandoned import can be restarted and a stalled import can
be retried.
## Current scope
- **Product decision:** Attachment binaries are intentionally not
imported in the current migration scope. Attachment metadata is
preserved and messages include a skipped-attachment marker.
- Adaptive Retry-After scheduling and handling the 30,000-ticket listing
ceiling are covered by stacked follow-up PRs.
## 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
## Description
When `POST /api/v1/accounts/:id/conversations/filter` fails (for example
a 500 or a server-side timeout), the conversation list spinner never
clears because the Vuex action's catch block was empty. The user is
stuck on an infinite loader with no way to retry.
This change:
- Clears the list loading state (`CLEAR_LIST_LOADING_STATUS`) and
rethrows in `fetchFilteredConversations`, matching how sibling actions
in the module handle errors. Both applied filters and saved filters
(custom views) go through this action.
- Catches the rejection in `ChatList.vue` and shows an alert ("Couldn't
load conversations. Please try again.") so the user can retry.
- Guards the reconnect flow in `ReconnectService` so a failed filtered
fetch on websocket reconnect does not abort cache revalidation.
- Fixes the existing spec for the success path, which passed no
`dispatch` and was silently exercising the error path.
No client-side request timeout was added since the dashboard API layer
has no per-request timeout convention; the fix is scoped to error
handling.
Fixes https://linear.app/chatwoot/issue/CW-7831
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added a spec asserting the loading state clears and the error is
rethrown when the filter request rejects: `pnpm test
app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js`
(49 passed).
- `pnpm test
app/javascript/dashboard/helper/specs/ReconnectService.spec.js` (24
passed).
## 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
Captain V2 now adds FAQ citations from a structured model response. The
model returns ordered response parts with citation indexes, and Chatwoot
turns only trusted indexes into customer links.
## Before
Captain V2 asked the model to copy text markers such as `[[faq:1]]`.
Chatwoot used one regular expression to replace those markers with links
in the outgoing message and another regular expression to remove the
rendered links before the next model turn. Long conversations depended
on parsing the customer message to recover plain model context.
## After
The FAQ lookup tool now gives each eligible source document a numeric
index and never gives the model a URL. FAQ results from the same
document reuse the same index. The model returns `response_parts`, where
each part contains customer text and the supporting citation indexes.
Chatwoot checks every index against the document IDs registered during
the current run.
Only stored HTTP or HTTPS web-document links without embedded
credentials can appear in the customer reply. Blank links, PDF sources,
attachments, non-HTTP storage links, and unknown indexes do not create
links. Sources receive display numbers in the order they first appear,
and repeated sources keep the same display number.
Chatwoot saves the structured response parts with each newly generated
Captain message. Later Captain V2 turns use the saved plain text for
those messages, so they never need to parse rendered links. Existing
messages remain unchanged and continue to use their stored content. When
citations are disabled, Chatwoot clears citation indexes before it
returns or saves the response.
Captain V1, Copilot, legacy prompts, legacy tools, and the playground
response contract are unchanged. The playground continues to show the
plain `response` field.
## Closes
[AI-138](https://linear.app/chatwoot/issue/AI-138/faq-citation-fix)
## How to test
1. Open a conversation handled by a Captain V2 assistant and turn
citations off. Ask a greeting, an FAQ question, a code question, and a
follow up question. Confirm that the assistant answers normally and
shows no source links.
2. Turn citations on and ask a question that matches one public web
document. Confirm that the reply shows the stored public link after the
supported text.
3. Ask a question that needs two public web documents. Confirm that the
response order stays correct, each link appears after the supported
text, and repeated sources keep the same display number.
4. Ask a question that retrieves several FAQ results from one document.
Confirm that the reply shows the document once at each supported
response part rather than exposing separate FAQ sources.
5. Ask a question supported by a PDF, attachment, blank link, or
non-HTTP storage link. Confirm that Captain can use the information but
does not show a customer link.
6. Ask for a fenced code example with a citation. Confirm that the code
block stays complete and the citation appears after the closing fence.
7. Continue the conversation with a follow up question. Confirm that
Captain uses the earlier plain response text and does not receive or
repeat rendered citation links.
8. Test a scenario handoff in a conversation. Confirm that the handoff
and final response still work.
This restores the temporary Meta incident safeguards with two
independent runtime controls for Chatwoot Cloud. Super admins can now
re-enable Instagram messaging first and keep new Meta inbox creation
disabled until onboarding is stable.
When messaging is disabled, Instagram conversations show the incident
notice and remain in private-note mode. The separate inbox-creation
control disables Facebook, Instagram, and WhatsApp Embedded Signup entry
points. Self-hosted installations remain unchanged.
Related: https://github.com/chatwoot/chatwoot/pull/15210
Related: https://status.chatwoot.com/incident/976975
### Things to know
- `DISABLE_META_MESSAGE_SENDING` affects Instagram messaging only.
- `DISABLE_META_INBOX_CREATION` affects Facebook, Instagram, and
WhatsApp Embedded Signup inbox creation.
- Both controls default to `true` for the active incident and are
editable from Super Admin installation configs.
- The Cloud-only boundary is enforced by the dashboard configuration
getters.
### How to test
1. Open Super Admin → Installation Configs and set
`DISABLE_META_MESSAGE_SENDING` to `false`.
2. Reload an Instagram conversation and confirm the incident banner
disappears and the runtime restriction no longer forces private-note
mode.
3. Set the flag back to `true`, reload, and confirm the banner and
restriction return.
4. Set `DISABLE_META_INBOX_CREATION` to `false`, reload the Facebook,
Instagram, or WhatsApp Embedded Signup setup flow, and confirm
connection is enabled.
5. Set the flag back to `true`, reload, and confirm the incident notice
appears and connection is disabled.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Account administrators can now browse synced WhatsApp message templates
inside Settings without opening a provider dashboard. The page combines
native WhatsApp and Twilio WhatsApp templates, with channel and language
filters, search, a template preview drawer, and links to manage
templates at the provider.
Creating, updating, and deleting templates in Chatwoot remains out of
scope for this first version.
Related:
https://linear.app/chatwoot/issue/PLA-193/add-whatsapp-template-listing-to-account-settings
<img width="2742" height="1460" alt="CleanShot 2026-08-03 at 20 23
27@2x"
src="https://github.com/user-attachments/assets/5d0b7a98-f882-425a-b9ec-7d631371959c"
/>
### Things to know
- This is stack 2 of 2 and depends on API draft #15311.
- Review this PR against `codex/whatsapp-template-api`; after the API PR
lands, this branch can be rebased and retargeted to `develop`.
- Both native WhatsApp and Twilio WhatsApp template caches are
supported.
- Template management remains in Meta Business Portfolio or Twilio
Console.
### How to test
1. Open Settings → WhatsApp templates for an account with native and
Twilio WhatsApp inboxes.
2. Confirm templates from each inbox are listed with their status,
language, category, channel, and last-updated metadata.
3. Filter by channel and language, and search by template name or
content.
4. Open a template and confirm its preview renders in the drawer.
5. Use the provider action and confirm it opens the appropriate Meta or
Twilio template-management page.
6. Confirm loading, empty, and error states remain usable.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Fixes the three `FullEditor.spec.js` slash-menu failures that appeared
after #15291 and #15296 crossed on develop: #15296 added slash-menu
specs that assert real translated labels ("Divider", "Heading 1"), while
#15291 emptied the global i18n catalogue in the vitest setup, so `t()`
started returning raw keys like `SLASH_COMMANDS.DIVIDER`.
The spec now calls `withFullI18n()` from the `test-i18n` helper
introduced in #15291, opting into the full message catalogue the same
way `MacroProperties.spec.js` does. Test-only change, no production code
touched.
Related: https://github.com/chatwoot/chatwoot/pull/15291
## Description
`vitest.setup.js` runs for every spec file and builds the i18n instance
from the full message catalogue, so each of the 389 spec files resolves
and transforms the 2537 JSON files under
`app/javascript/dashboard/i18n/locale/`. With `pool: 'threads'`, that
cost is paid per worker. The result is that `setup` takes about **60x
longer than the tests themselves**.
Only one spec in the suite asserts on translated copy. This PR leaves
the global i18n instance without messages and adds `withFullI18n`, an
opt-in helper for the specs that need the real catalogue.
**On this repository's own CI, `setup` drops from ~629s to ~71s (-89%)
and total Vitest duration from ~388s to ~196s (-49%).**
This is a performance and testability change, not a cosmetic one. It
touches only how the test harness loads messages — **no translatable
string and no locale file is modified**, so nothing changes for Crowdin
contributors.
Worth noting: `vitest.config.ts` already excludes `**/i18n/**/*` from
coverage, so loading the catalogue in every spec was not serving any
metric.
Fixes#15290
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### On this repository's CI
The numbers below come from the `test` job of `frontend-fe.yml` on
GitHub Actions — this repo's own runners, not a local machine. Baseline
is **60 successful `develop` runs** (23–31 Jul 2026), parsed from the
Vitest summary line in each job log.
| Vitest metric | `develop` (n=60) | This PR | Delta |
|---|---|---|---|
| **setup** | median **629s** (min 329 / p25 607 / p75 641 / max 680) |
**71s** | **-559s (-89%)** |
| **duration** | median **388s** (min 205 / p25 371 / p75 393 / max 418)
| **196s** | **-192s (-49%)** |
None of the 60 `develop` runs beat this PR on either metric.
Same commit, both on this repo's CI:
```
develop @ bc7ae88 Test Files 389 passed (389)
Duration 323.35s (transform 13.75s, setup 522.48s, collect 42.53s,
tests 9.02s, environment 187.05s, prepare 32.77s)
this PR Test Files 389 passed (389)
Duration 196.20s (transform 18.18s, setup 70.67s, collect 65.97s,
tests 12.22s, environment 251.28s, prepare 43.90s)
```
`setup` is the metric that isolates this change: it is a sum of work, so
it is not distorted by how fast a given runner happens to be. `duration`
improves less because it also covers transform, collect and environment,
which this change does not touch.
One caveat if you compare total job times instead: on this PR's run,
`ruby/setup-ruby` took 130s versus 11s on the baseline (an unrelated
cache miss), which hides most of the gain at job level.
### Locally
`pnpm exec vitest run`, twice per scenario, on `develop` at `bc7ae88`
(Node 24.18.1, macOS arm64):
| | setup | duration |
|---|---|---|
| before | 373s / 412s | 60.8s / 67.8s |
| after | 40.5s / 42.8s | 28.1s / 28.5s |
Identical pass/fail counts before and after. Locally there is one
failure in both scenarios, pre-existing on `develop` and unrelated to
this change: `useReportMetrics.spec.js` expects `'5,000'` and receives
`'5.000'`, a thousands-separator difference that depends on the machine
locale. It does not occur on CI, where the suite is fully green
(389/389).
`MacroProperties.spec.js` was the only spec that depended on the global
catalogue — it asserts the real copy from `macros.json`. It now calls
`withFullI18n()` and keeps asserting the same strings, so coverage of
that copy is preserved.
## Notes for reviewers
- The `test-i18n` alias was added to `vitest.config.ts` rather than to
`vite.shared.ts`, to keep it out of the production build.
- `missingWarn: false` and `fallbackWarn: false` were added to the
global instance so specs that render translated components without
opting in do not flood the output with missing-key warnings.
- Any future spec that needs the real copy just calls `withFullI18n()`
at the top of the file.
## 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 (no new test: this is a performance change, verified by
the CI measurements above and by the unchanged pass/fail counts)
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
WhatsApp template messages can be delivered with the correct variable
values while the Chatwoot conversation shows numeric placeholders
instead. This affects template sends where the client submits the raw
template body together with valid processed parameters; WhatsApp
delivery itself remains unchanged.
The message model currently passes outgoing content through Liquid
before saving it. Liquid interprets positional WhatsApp placeholders
such as `{{1}}` and `{{2}}` as numeric expressions, even though the
WhatsApp sender independently uses `processed_params.body` to build the
provider payload.
Fixes
https://linear.app/chatwoot/issue/PLA-192/render-whatsapp-template-values-in-conversation-transcripts
### What changed
For WhatsApp template messages, Chatwoot now substitutes positional or
named body placeholders from `processed_params.body` before saving the
transcript. Missing values remain visible as placeholders, and ordinary
outgoing Liquid messages continue through the existing rendering path.
The existing message model regression suite remains green, and the
reported payload now persists as `Hello Ahmad, Furqan is your contact.`
### Things to know
This corrects newly created messages. Existing conversation messages
that were already stored as `1`, `2`, and so on are not backfilled.
### How to reproduce
1. Open a WhatsApp conversation outside the 24-hour messaging window.
2. Send a positional template whose body contains `{{1}}` and `{{2}}`.
3. Supply values for both parameters while submitting the original
template body as the message content.
4. Observe that WhatsApp delivers the substituted values, while the
Chatwoot transcript shows `1` and `2`.
### How to test
1. Select a WhatsApp template with two body variables.
2. Enter `Ahmad` and `Furqan` as the values and send the message.
3. Confirm the recipient receives the substituted template.
4. Confirm the Chatwoot conversation also displays `Ahmad` and `Furqan`
instead of the numeric placeholders.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This adds a reusable side drawer component
(`components-next/drawer/Drawer.vue`), similar in spirit to Dialog and
Popover, so drawers across the app share one implementation. The drawer
renders as a floating card anchored to the inline-end edge with a
slide-in/slide-out animation, and handles the backdrop, Escape key,
click-outside, and focus restore. The layout inside is fully
slot-driven, with a `close` function passed through the slot.
The Captain overview and report drilldown drawers now use this
component, and the Captain document details view moved from a centered
dialog to this drawer.
## Closes
CW-7757
## What changed
- New `Drawer.vue`: teleported floating card with backdrop, RTL-aware
slide transition, focus management, and a default slot receiving
`close`; emits `afterLeave` so consumers mounted with `v-if` can unmount
after the exit animation
- `AssistantDrilldownDrawer` and `ReportDrilldownDrawer` refactored to
consume it, keeping their own headers and content
- `DocumentDetails` migrated from Dialog to the drawer, dropping the
imperative `dialogRef.open()` plumbing and fixed-height inner scroll
areas
- Shared `DRAWER.CLOSE` i18n key replaces the per-drawer close labels
https://github.com/user-attachments/assets/58db6c34-8b6f-46e2-bdb1-f2d3675f1a94
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Required checkbox fields in the pre-chat form now remain invalid unless
they are checked. Other required field types keep their existing
validation behavior, and the existing localized required message is
reused.
## Closes
Closes https://github.com/chatwoot/chatwoot/issues/15128
## How to reproduce
1. Add a required checkbox custom attribute to the pre-chat form.
2. Check and then uncheck it.
3. Submit the form; submission is now blocked until the checkbox is
checked.
## What changed
- Use the FormKit accepted rule for required checkbox fields.
- Map accepted validation failures to the existing pre-chat required
message.
---------
Co-authored-by: Nazmus Samir <nazmussamir@Nazmuss-MacBook-Pro.local>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Adds a dashboard-only takeover path for pending conversations currently
handled by an assistant/bot. Agents see the warning by default, stay in
private-note mode while the conversation is pending, and can use Take
over to move the conversation back to human handling.
## Related
- Original scope:
https://linear.app/chatwoot/issue/CW-7450/block-replies-and-add-takeover-for-agent-bot-ownership
- Backend follow-up:
https://linear.app/chatwoot/issue/CW-7779/enforce-backend-reply-blocking-for-agent-bot-owned-conversations
## Why
We want to prevent accidental parallel handling from the dashboard while
an assistant is managing a pending conversation, without expanding this
PR into API-level enforcement. Backend blocking is tracked separately in
CW-7779.
## What changed
- Locks the dashboard composer to private-note mode while the
conversation status is `pending`.
- Shows the takeover banner by default for pending conversations.
- Uses the Agent Bot assignee name when the conversation payload exposes
one, otherwise falls back to `a bot`.
- Simplifies the banner action copy to `Take over`.
- Reopens and self-assigns the conversation from the takeover action.
- Clears stale local `AgentBot` assignee type when assigning the
conversation back to a human in the store.
## How to test
- Open a pending conversation assigned to an Agent Bot and verify the
banner says it is handled by that bot name.
- Verify the reply editor stays in private-note mode and public reply
mode cannot be selected while the conversation is pending.
- Click Take over and verify the conversation moves to open and is
assigned to the current agent.
- Open a pending Captain/Dialogflow-style conversation without an Agent
Bot assignee payload and verify the banner falls back to `a bot`.
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>