Keeps the inbox conversation and contact sidebar within the available
viewport at desktop breakpoints.
## Preview
https://github.com/user-attachments/assets/aba1e25e-8741-4544-8659-a6024ad1e887
## How to reproduce
1. Open an inbox notification with the contact sidebar visible.
2. Resize the viewport around the 1536px breakpoint or expand the main
navigation.
3. Confirm the inbox list, conversation, and contact sidebar remain
visible.
## Summary
Adds conversation usage to Captain documents and user created FAQs. Only
knowledge used in a Captain answer sent to the contact is counted.
Lookups that end in a handoff are excluded.
Administrators can see how many distinct conversations used a knowledge
record. Document usage appears in the Usage tab inside document details.
User created FAQ usage appears on each FAQ card and opens in a
conversation panel.
Deleted conversations are excluded from counts, sorting, and
conversation lists.
Usage shown in side panel
<img width="2342" height="1502" alt="CleanShot 2026-08-12 at 18 29
02@2x"
src="https://github.com/user-attachments/assets/67301051-6df5-4eb6-9b8d-e1fc75bb02f2"
/>
Sorting options
<img width="580" height="241" alt="image"
src="https://github.com/user-attachments/assets/078566b7-481b-491b-b484-c2fef481f1b1"
/>
## Access
Conversation usage is available only to administrators. Agents can still
view documents and FAQs, but they cannot see usage counts, the Usage
tab, the "Most used" sort, or conversation usage details.
The API applies the same rule. An agent request for `sort=most_used` is
rejected.
## Performance and pagination
Document and FAQ lists return 25 records per page. Usage counts are
calculated only for the records in each page.
Conversation usage panels load 25 conversations at a time and show a
"Load more" action when more conversations are available. The generated
FAQs tab also keeps its existing 25 item pagination.
The count queries use the JSON indexes on `document_ids` and
`used_faq_ids`. The "Most used" sort aggregates one assistant's sessions
once before it sorts and returns the requested document page.
## How to test
1. Sign in as an administrator and open the Captain documents page for
an assistant with tracked document usage.
2. Open a document and confirm that the Usage tab shows the distinct
conversation count and the matching conversations.
3. Select "Most used" and confirm that documents are ordered by distinct
conversation usage.
4. Open the user created FAQs page and confirm that FAQ cards show their
usage count and open the matching conversations.
5. Confirm that usage panels show 25 conversations first and can load
the next page.
6. Trigger a knowledge lookup that ends in a handoff and confirm that it
does not increase document or FAQ usage.
7. Sign in as an agent and confirm that usage counts, usage details, and
the "Most used" sort are not available.
## Closes
[CW-7498](https://linear.app/chatwoot/issue/CW-7498/fe)
## Description
The shared duration input enforced its minimum after every keypress.
When the minimum was 10, typing `45` changed the first digit to `10`, so
the second digit produced `105`.
This change lets people finish typing before the input applies its
minimum and maximum. The input now normalizes the value when it loses
focus or when the person presses Enter.
## Type of change
- [x] Bug fix
## How has this been tested?
- Added a component test that types `4`, then `5`, and confirms that the
value stays `45`.
- Confirmed that values below 10 and above 100 are normalized when the
input loses focus.
- Ran the focused Vitest file and ESLint.
- Ran a local deterministic Playwright test through the delayed
automation form. The test typed `45`, saved the rule, confirmed
`execution_delay: 45` in the API response, confirmed `Runs after 45m` in
the list, and removed the test rule.
## Checklist
- [x] My code follows the style guidelines of this project.
- [x] I have performed a self-review of my code.
- [x] I have added tests that prove the fix works.
- [x] The focused unit and browser tests pass locally.
# Pull Request Template
## Description
This PR fixes the reply editor staying at its default height when
Copilot generates a suggestion, causing longer suggestions to be clipped
after a line or two.
The editor now automatically grows to fit the suggestion, up to a
maximum of 350px, and returns to the height it had before the suggestion
when it is accepted or discarded. Manual resizing continues to work as
expected and always takes priority.
### How to reproduce
1. Open a conversation and generate a Copilot suggestion (✨ → Summarize
the conversation).
2. The reply editor stays at its default height and the suggestion gets
clipped after a line or two.
3. Resize the reply editor, generate another suggestion, and discard it.
The editor no longer returns to the height you set.
### What changed
* `CopilotEditor` measures the rendered suggestion and requests enough
space to display it, capped at 350px. The requested height is released
when the suggestion is gone.
* `ResizableEditorWrapper` handles this requested height separately from
the manually dragged height, so the suggestion can grow the editor
without overriding the user's preferred height.
* The loading and suggestion states now cross-fade in place while the
editor resizes, instead of briefly switching through an empty card.
### Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screencast
https://github.com/user-attachments/assets/d9a5dab9-6206-4333-92d1-d3f710f9022e
## 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
The WhatsApp health status page fails to render for accounts using
underscore-based regional locales such as `pt_BR`. The browser rejects
that locale when formatting the last-onboarded timestamp, leaving the
page blank even though health data loaded successfully.
This change formats the timestamp with Chatwoot’s shared resolved
locale, which normalizes and validates the locale before passing it to
the browser. Other health status behavior remains unchanged.
### How to reproduce
1. Set the dashboard language to Portuguese (Brazil).
2. Open an inbox’s WhatsApp health status page when the response
includes `last_onboarded_time`.
3. Observe that the page remains blank with `RangeError: Invalid
language tag: pt_BR` in the browser console.
### How to test
1. Use an account with Portuguese (Brazil) selected.
2. Open the WhatsApp health status page for an inbox with onboarding
data.
3. Confirm the complete health status page loads and the last-onboarded
timestamp is formatted in the selected locale.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
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